diff --git a/examples/cronos.ts b/examples/cronos.ts new file mode 100644 index 0000000..2d5dbd6 --- /dev/null +++ b/examples/cronos.ts @@ -0,0 +1,87 @@ +import { Kiln, KILN_VALIDATORS, croToBasecro } from '../src/kiln.ts'; +import type { FireblocksIntegration } from '../src/fireblocks.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 fireblocksPubkey = ( + await k.fireblocks.getSdk(vault).vaults.getPublicKeyInfo({ + algorithm: 'MPC_ECDSA_SECP256K1', + derivationPath: JSON.stringify([44, 394, Number(vault.vaultId), 0, 0]), + compressed: true, + }) +).data.publicKey; +if (!fireblocksPubkey) { + console.log('Failed to get pubkey'); + process.exit(0); +} + +// +// Craft the transaction +// +console.log('Crafting transaction...'); +const txRequest = await k.client.POST('/cro/transaction/stake', { + body: { + account_id: kilnAccountId, + pubkey: fireblocksPubkey, + validator: KILN_VALIDATORS.CRO.mainnet.KILN, + amount_basecro: croToBasecro('0.01').toString(), + restake_rewards: false, + }, +}); +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.signCroTx(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('/cro/transaction/broadcast', { + body: { + tx_serialized: signRequest.signed_tx.data.signed_tx_serialized, + }, +}); +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 2a1b922..7284317 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kilnfi/sdk", - "version": "4.1.3", + "version": "4.1.4", "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 a43c04f..0c0d478 100644 --- a/src/fireblocks.ts +++ b/src/fireblocks.ts @@ -506,6 +506,61 @@ export class FireblocksService { }; } + /** + * Sign a CRO transaction on Fireblocks + */ + async signCroTx( + integration: FireblocksIntegration, + tx: components['schemas']['CROUnsignedTx'] | components['schemas']['CROStakeUnsignedTx'], + note?: string, + ): Promise<{ + signed_tx: { data: components['schemas']['CROSignedTx'] }; + fireblocks_tx: TransactionResponse; + }> { + const payload = { + rawMessageData: { + messages: [ + { + content: tx.unsigned_tx_hash, + derivationPath: [44, 394, integration.vaultId, 0, 0], + preHash: { + content: tx.unsigned_tx_serialized, + hashAlgorithm: 'SHA256', + }, + }, + ], + algorithm: SignedMessageAlgorithmEnum.EcdsaSecp256K1, + }, + }; + + const fbSigner = this.getSigner(integration); + const fbNote = note ? note : 'CRO tx from @kilnfi/sdk'; + const fbTx = await fbSigner.sign(payload, undefined, fbNote); + const signature = fbTx.signedMessages?.[0]?.signature?.fullSig; + + if (!signature) { + throw new Error(ERRORS.MISSING_SIGNATURE); + } + + const preparedTx = await this.client.POST('/cro/transaction/prepare', { + body: { + pubkey: tx.pubkey, + tx_body: tx.tx_body, + tx_auth_info: tx.tx_auth_info, + signature: signature, + }, + }); + + if (preparedTx.error) { + throw new Error(ERRORS.FAILED_TO_PREPARE); + } + + return { + signed_tx: preparedTx.data, + fireblocks_tx: fbTx, + }; + } + /** * Sign a NOBLE transaction on Fireblocks */ diff --git a/src/utils.ts b/src/utils.ts index df9b1d7..addd1fc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -78,6 +78,12 @@ export const omToUom = (om: string): bigint => { return parseUnits(om, 6); }; +/** + * Convert CRO to basecro + */ +export const croToBasecro = (cro: string): bigint => { + return parseUnits(cro, 8); +}; /** * Convert uZETA to ZETA */ @@ -204,6 +210,13 @@ export const uomToOm = (uom: bigint): string => { return formatUnits(uom, 6); }; +/** + * Convert basecro to CRO + */ +export const basecroToCro = (basecro: bigint): string => { + return formatUnits(basecro, 8); +}; + /** * Get a cosmos address from its public key and prefix * @param pubkey diff --git a/src/validators.ts b/src/validators.ts index 2eb5bcc..671457d 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -90,6 +90,11 @@ export const KILN_VALIDATORS = { KILN: 'mantravaloper146mj09yzu3mvz7pmy4dvs4z9wr2mst7ram37xw', }, }, + CRO: { + mainnet: { + KILN: 'crocncl1lqm9hhzuqgutgswlff5yf9kmn7vx6pmksk4kvs', + }, + }, OSMO: { mainnet: { INTEROP: 'osmovaloper146mj09yzu3mvz7pmy4dvs4z9wr2mst7rq8p8gy',