diff --git a/examples/sui.ts b/examples/sui.ts new file mode 100644 index 0000000..812c8a9 --- /dev/null +++ b/examples/sui.ts @@ -0,0 +1,93 @@ +import type { FireblocksIntegration } from '../src/fireblocks.ts'; +import { KILN_VALIDATORS, Kiln, suiToMist } from '../src/kiln.ts'; +import { loadEnv } from './env.ts'; + +const { kilnApiKey, kilnAccountId, kilnApiUrl, fireblocksApiKey, fireblocksApiSecret, fireblocksVaultId } = + await loadEnv(); + +const k = new Kiln({ + baseUrl: kilnApiUrl, + apiToken: kilnApiKey, +}); + +const vault: FireblocksIntegration = { + config: { + apiKey: fireblocksApiKey, + secretKey: fireblocksApiSecret, + basePath: 'https://api.fireblocks.io/v1', + }, + vaultId: fireblocksVaultId, +}; + +// +// Get the pubkey from Fireblocks +// +const fireblocksWallet = ( + await k.fireblocks + .getSdk(vault) + .vaults.getVaultAccountAssetAddressesPaginated({ assetId: 'SUI', vaultAccountId: vault.vaultId, limit: 1 }) +).data.addresses?.[0].address; +if (!fireblocksWallet) { + console.log('Failed to get pubkey'); + process.exit(0); +} + +console.log(fireblocksWallet); + +// +// Craft the transaction +// +console.log('Crafting transaction...'); +console.log('params:', { + account_id: kilnAccountId, + sender: fireblocksWallet, + validator_address: KILN_VALIDATORS.SUI.mainnet.KILN, + amount_mist: suiToMist('1.1').toString(), +}); +const txRequest = await k.client.POST('/sui/transaction/stake', { + body: { + account_id: kilnAccountId, + sender: fireblocksWallet, + validator_address: KILN_VALIDATORS.SUI.mainnet.KILN, + amount_mist: suiToMist('1.1').toString(), + }, +}); +if (txRequest.error) { + console.log('Failed to craft transaction:', txRequest); + process.exit(1); +} else { + console.log('Crafted transaction:', txRequest.data); +} +console.log('\n\n\n'); + +// +// Sign the transaction +// +console.log('Signing transaction...'); +const signRequest = await (async () => { + try { + return await k.fireblocks.signSuiTx(vault, txRequest.data.data); + } catch (err) { + console.log('Failed to sign transaction:', err); + process.exit(1); + } +})(); +console.log('Signed transaction:', signRequest); +console.log('\n\n\n'); + +// +// Broadcast the transaction +// +console.log('Broadcasting transaction...'); +const broadcastedRequest = await k.client.POST('/sui/transaction/broadcast', { + body: { + tx_serialized: signRequest.signed_tx.data.tx_serialized, + serialized_signature: signRequest.signed_tx.data.serialized_signature, + }, +}); +if (broadcastedRequest.error) { + console.log('Failed to broadcast transaction:', broadcastedRequest); + process.exit(1); +} else { + console.log('Broadcasted transaction:', broadcastedRequest.data); +} diff --git a/package.json b/package.json index 2b00aa5..db34ccf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kilnfi/sdk", - "version": "4.2.16", + "version": "4.2.17", "autor": "Kiln (https://kiln.fi)", "license": "BUSL-1.1", "description": "JavaScript sdk for Kiln API", diff --git a/src/fireblocks.ts b/src/fireblocks.ts index 00a5d5a..433eb85 100644 --- a/src/fireblocks.ts +++ b/src/fireblocks.ts @@ -21,6 +21,7 @@ export type FireblocksIntegration = ( const ERRORS = { MISSING_SIGNATURE: 'An error occurred while attempting to retrieve the signature from Fireblocks.', FAILED_TO_PREPARE: 'An error occurred while attempting to add the signature to the transaction.', + MISSING_PUBLIC_KEY: 'An error occurred while attempting to retrieve the public key from Fireblocks.', }; export class FireblocksService { @@ -1271,4 +1272,54 @@ export class FireblocksService { fireblocks_tx: fbTx, }; } + + async signSuiTx( + integration: FireblocksIntegration, + tx: components['schemas']['SUITx'], + note?: string, + ): Promise<{ + signed_tx: { data: components['schemas']['SUIBroadcastTxPayload'] }; + fireblocks_tx: TransactionResponse; + }> { + const payload = { + rawMessageData: { + messages: [ + { + content: tx.unsigned_tx_hash.substring(2), + }, + ], + }, + }; + + const fbSigner = this.getSigner(integration); + const fbNote = note ? note : 'SUI tx from @kilnfi/sdk'; + const fbTx = await fbSigner.sign(payload, 'SUI', fbNote); + const signature = fbTx.signedMessages?.[0]?.signature?.fullSig; + const fbPubkey = fbTx.signedMessages?.[0]?.publicKey; + + if (!signature) { + throw new Error(ERRORS.MISSING_SIGNATURE); + } + + if (!fbPubkey) { + throw new Error(ERRORS.MISSING_PUBLIC_KEY); + } + + const preparedTx = await this.client.POST('/sui/transaction/prepare', { + body: { + pubkey: fbPubkey, + signature: signature, + tx_serialized: tx.unsigned_tx_serialized, + }, + }); + + if (preparedTx.error) { + throw new Error(ERRORS.FAILED_TO_PREPARE); + } + + return { + signed_tx: preparedTx.data, + fireblocks_tx: fbTx, + }; + } } diff --git a/src/fireblocks_signer.ts b/src/fireblocks_signer.ts index d9960d4..a91c7a2 100644 --- a/src/fireblocks_signer.ts +++ b/src/fireblocks_signer.ts @@ -36,7 +36,8 @@ export type FireblocksAssetId = | 'KAVA_KAVA' | 'TRX' | 'BTC' - | 'SEI'; + | 'SEI' + | 'SUI'; export class FireblocksSigner { constructor( diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index 1c80d00..ffe0c15 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -8414,6 +8414,26 @@ export interface paths { patch?: never; trace?: never; }; + "/sui/transaction/prepare": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Prepare Transaction + * @description Prepare a transaction. + */ + post: operations["postSuiPrepareTx"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/sui/transaction/status": { parameters: { query?: never; @@ -40004,16 +40024,6 @@ export interface components { * @example 9020446847418 */ net_rewards: string; - /** - * @description Total withdrawn rewards by this stake since its first ever delegation - * @example 9020446847418 - */ - withdrawn_rewards: string; - /** - * @description Total withdrawable rewards by this stake - * @example 0 - */ - withdrawable_rewards: string; /** * @description The amount of SUI currently staked * @example 92908788559 @@ -40051,6 +40061,11 @@ export interface components { * @example 2.02 */ net_apy: number; + /** + * @description Stake Object ID involved in the operation + * @example 0x23f9f0342c9ee0c7e74df0dc6655d2ae672ae08fe12dc4ac2d604074687555a3 + */ + stake_id: string; }; SUIRewardByEpoch: { /** @@ -40305,6 +40320,20 @@ export interface components { */ digest: string; }; + SUIPrepareTx: { + /** + * Format: base64 + * @description Serialized transaction as base64 encoded bcs + * @example AAACAAgAypo7AAAAAAAg2+ZDD9PphmfRs1A4Aba3bIThZOG+V+8OS/Pjq3kbI9YCAgABAQAAAQEDAAAAAAEBABsdj13zFOK3uKhs/ir/eMwbwKDsykcidGrlIiYx0U9vAjWzex7zdLfWq8gj/oP81sYt3UJzyo4bDhvRJ7E1R/tVd1fpIAAAAAAgfCKhipjlwhR0OghzVijOV+b+CFucrTVw1y6LK+g4dXUM4X57NnqLr5kjWC0veDHxiAJG0cKrkJDEubjYQ6JXundX6SAAAAAAICh1unSYqH9yQqaJuKEftzLEGT5rqV7wHUD16BMQcmLAGx2PXfMU4re4qGz+Kv94zBvAoOzKRyJ0auUiJjHRT2/oAwAAAAAAAECrPAAAAAAAAA== + */ + tx_serialized: string; + /** + * Format: base64 + * @description Base64-encoded Sui serialized signature. + * @example AMQ2b2LwCWca7IK5hY1lnzkhRwb4nkYCTA3on08RpMEA6myGSgTWmBH2KDLZmaXzSXIVCNKrP3dCzBZvM3gRzTJq3RpEQpcj32BYljGTj4jFrXXGGPohME56ZK2MBDw== + */ + serialized_signature: string; + }; SUITxStatus: { /** * @description Transaction status @@ -40396,15 +40425,34 @@ export interface components { SUIBroadcastTxPayload: { /** * Format: base64 - * @description Signed serialized transaction as base64 encoded bcs + * @description Serialized transaction as base64 encoded bcs * @example AAACAAgAypo7AAAAAAAg2+ZDD9PphmfRs1A4Aba3bIThZOG+V+8OS/Pjq3kbI9YCAgABAQAAAQEDAAAAAAEBABsdj13zFOK3uKhs/ir/eMwbwKDsykcidGrlIiYx0U9vAjWzex7zdLfWq8gj/oP81sYt3UJzyo4bDhvRJ7E1R/tVd1fpIAAAAAAgfCKhipjlwhR0OghzVijOV+b+CFucrTVw1y6LK+g4dXUM4X57NnqLr5kjWC0veDHxiAJG0cKrkJDEubjYQ6JXundX6SAAAAAAICh1unSYqH9yQqaJuKEftzLEGT5rqV7wHUD16BMQcmLAGx2PXfMU4re4qGz+Kv94zBvAoOzKRyJ0auUiJjHRT2/oAwAAAAAAAECrPAAAAAAAAA== */ tx_serialized: string; /** * Format: base64 - * @description Base64 encoded signature of the transaction + * @description Base64-encoded Sui serialized signature. * @example AMQ2b2LwCWca7IK5hY1lnzkhRwb4nkYCTA3on08RpMEA6myGSgTWmBH2KDLZmaXzSXI+++VCNKrP3dCzBZvM3gRzTJq3RpEQpcj32BYljGTj4jFrXXGGPohME56ZK2MBDw== */ + serialized_signature: string; + }; + SUIPrepareTxPayload: { + /** + * @description Wallet public key, this is different than the wallet address + * @example 039ce47b2a813d13876131a9c3be77e8c4afa49e948744abbee11f939e2a420f46 + */ + pubkey: string; + /** + * Format: base64 + * @description Serialized transaction as base64 encoded bcs + * @example AAACAAgAypo7AAAAAAAg2+ZDD9PphmfRs1A4Aba3bIThZOG+V+8OS/Pjq3kbI9YCAgABAQAAAQEDAAAAAAEBABsdj13zFOK3uKhs/ir/eMwbwKDsykcidGrlIiYx0U9vAjWzex7zdLfWq8gj/oP81sYt3UJzyo4bDhvRJ7E1R/tVd1fpIAAAAAAgfCKhipjlwhR0OghzVijOV+b+CFucrTVw1y6LK+g4dXUM4X57NnqLr5kjWC0veDHxiAJG0cKrkJDEubjYQ6JXundX6SAAAAAAICh1unSYqH9yQqaJuKEftzLEGT5rqV7wHUD16BMQcmLAGx2PXfMU4re4qGz+Kv94zBvAoOzKRyJ0auUiJjHRT2/oAwAAAAAAAECrPAAAAAAAAA== + */ + tx_serialized: string; + /** + * Format: base64 + * @description Hex-encoded raw signature bytes. + * @example a11afeea9af497fdae1caa2c02cf5f1b964251093ee7acd47a7193d991c64eefb3d9879a3fa3015f2003b0d61b95bdf9de1f5f155fac6be4bbe058cdcda4c60b + */ signature: string; }; SUIDecodeTxPayload: { @@ -40506,6 +40554,11 @@ export interface components { * @description Base64-encoded payload data of the operation */ data: string; + /** + * @description Stake Object ID involved in the operation + * @example 0x23f9f0342c9ee0c7e74df0dc6655d2ae672ae08fe12dc4ac2d604074687555a3 + */ + stake_id: string; }; SEIStake: { /** @@ -42668,6 +42721,8 @@ export interface components { SUIDelegatorsParam: string[]; /** @description Comma-separated list of validator addresses */ SUIValidatorsParam: string[]; + /** @description Comma-separated list of stake object ids */ + SUIStakeIdsParam: string[]; /** @description Transaction hash */ SUITxHashParam: string; /** @description Comma-separated list of validators addresses, these addresses @@ -63775,6 +63830,8 @@ export interface operations { delegators?: components["parameters"]["SUIDelegatorsParam"]; /** @description Comma-separated list of validator addresses */ validators?: components["parameters"]["SUIValidatorsParam"]; + /** @description Comma-separated list of stake object ids */ + stake_ids?: components["parameters"]["SUIStakeIdsParam"]; /** @description Comma-separated list of Kiln accounts identifiers */ accounts?: components["parameters"]["AccountsParam"]; }; @@ -63825,6 +63882,8 @@ export interface operations { delegators?: components["parameters"]["SUIDelegatorsParam"]; /** @description Comma-separated list of validator addresses */ validators?: components["parameters"]["SUIValidatorsParam"]; + /** @description Comma-separated list of stake object ids */ + stake_ids?: components["parameters"]["SUIStakeIdsParam"]; /** @description The format of the response. Defaults to `daily` */ format?: components["parameters"]["SUIRewardsFormatParam"]; /** @description Comma-separated list of Kiln accounts identifiers */ @@ -63883,6 +63942,8 @@ export interface operations { delegators?: components["parameters"]["SUIDelegatorsParam"]; /** @description Comma-separated list of validator addresses */ validators?: components["parameters"]["SUIValidatorsParam"]; + /** @description Comma-separated list of stake object ids */ + stake_ids?: components["parameters"]["SUIStakeIdsParam"]; /** @description Transaction hash */ tx_hash?: components["parameters"]["SUITxHashParam"]; /** @description Comma-separated list of Kiln accounts identifiers */ @@ -64256,6 +64317,54 @@ export interface operations { }; }; }; + postSuiPrepareTx: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Prepare a transaction on SUI. */ + requestBody: { + content: { + "application/json; charset=utf-8": components["schemas"]["SUIPrepareTxPayload"]; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": { + data: components["schemas"]["SUIPrepareTx"]; + }; + }; + }; + /** @description Invalid parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; getSuiTxStatus: { parameters: { query: { diff --git a/src/utils.ts b/src/utils.ts index eb4efc7..3de9ec6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -336,3 +336,17 @@ export const seiToUsei = (sei: string): bigint => { export const useiToSei = (usei: bigint): string => { return formatUnits(usei, 6); }; + +/** + * Convert SUI to mist + */ +export const suiToMist = (sui: string): bigint => { + return parseUnits(sui, 9); +}; + +/** + * Convert mist to SUI + */ +export const mistToSui = (mist: bigint): string => { + return formatUnits(mist, 9); +}; diff --git a/src/validators.ts b/src/validators.ts index 998df2c..8439c40 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -105,4 +105,9 @@ export const KILN_VALIDATORS = { KILN: 'seivaloper1u9xeaqdjz3kky2ymdhdsn0ra5uy9tc3eqkjc8w', }, }, + SUI: { + mainnet: { + KILN: '0x92c7bf9914897e8878e559c19a6cffd22e6a569a6dd4d26f8e82e0f2ad1873d6', + }, + }, };