From d1dac44e2e36451d352bfea386710d3f2c363b9c Mon Sep 17 00:00:00 2001 From: Noel Hawat Date: Thu, 25 Sep 2025 12:24:53 -0400 Subject: [PATCH] fix(sdk-coin-avaxp): update utxo selection to also check STAKEABLE_LOCK_OUT noticed that some utxo will be of type 22 with locktim 0 those are eligible to be used for transactions TICKET: SC-3257 --- .../src/lib/atomicTransactionBuilder.ts | 7 ++-- .../src/lib/delegatorTxBuilder.ts | 7 ++-- modules/sdk-coin-avaxp/src/lib/iface.ts | 8 ++++- .../lib/permissionlessValidatorTxBuilder.ts | 33 ++++--------------- .../src/lib/transactionBuilder.ts | 9 ----- modules/sdk-coin-avaxp/src/lib/utxoEngine.ts | 9 +++-- .../sdk-coin-avaxp/test/resources/avaxp.ts | 2 +- 7 files changed, 32 insertions(+), 43 deletions(-) diff --git a/modules/sdk-coin-avaxp/src/lib/atomicTransactionBuilder.ts b/modules/sdk-coin-avaxp/src/lib/atomicTransactionBuilder.ts index 5ea6f78b1b..9a88e82f4a 100644 --- a/modules/sdk-coin-avaxp/src/lib/atomicTransactionBuilder.ts +++ b/modules/sdk-coin-avaxp/src/lib/atomicTransactionBuilder.ts @@ -11,7 +11,7 @@ import { } from 'avalanche/dist/apis/platformvm'; import { Credential } from 'avalanche/dist/common'; import { BuildTransactionError } from '@bitgo/sdk-core'; -import { SECP256K1_Transfer_Output } from './iface'; +import { SECP256K1_STAKEABLE_LOCK_OUT, SECP256K1_Transfer_Output } from './iface'; /** * Cross-chain transactions (export and import) are atomic operations. @@ -104,7 +104,10 @@ export abstract class AtomicTransactionBuilder extends DeprecatedTransactionBuil }); this.transaction._utxos.forEach((utxo, i) => { - if (utxo.outputID === SECP256K1_Transfer_Output) { + if ( + utxo.outputID === SECP256K1_Transfer_Output || + (utxo.outputID === SECP256K1_STAKEABLE_LOCK_OUT && utxo.locktime === '0') + ) { const txidBuf = utils.cb58Decode(utxo.txid); const amt: BN = new BN(utxo.amount); const outputidx = utils.outputidxNumberToBuffer(utxo.outputidx); diff --git a/modules/sdk-coin-avaxp/src/lib/delegatorTxBuilder.ts b/modules/sdk-coin-avaxp/src/lib/delegatorTxBuilder.ts index c84531ae11..7c13aea74f 100644 --- a/modules/sdk-coin-avaxp/src/lib/delegatorTxBuilder.ts +++ b/modules/sdk-coin-avaxp/src/lib/delegatorTxBuilder.ts @@ -16,7 +16,7 @@ import { UnsignedTx, } from 'avalanche/dist/apis/platformvm'; import { BinTools, BN } from 'avalanche'; -import { SECP256K1_Transfer_Output, DeprecatedTx, DeprecatedBaseTx } from './iface'; +import { SECP256K1_Transfer_Output, DeprecatedTx, DeprecatedBaseTx, SECP256K1_STAKEABLE_LOCK_OUT } from './iface'; import utils from './utils'; import { Credential } from 'avalanche/dist/common'; import { deprecatedRecoverUtxos } from './utxoEngine'; @@ -309,7 +309,10 @@ export class DelegatorTxBuilder extends DeprecatedTransactionBuilder { const buildOutputs = this.transaction._utxos[0].addresses.length !== 0; this.transaction._utxos.forEach((utxo, i) => { - if (utxo.outputID === SECP256K1_Transfer_Output) { + if ( + utxo.outputID === SECP256K1_Transfer_Output || + (utxo.outputID === SECP256K1_STAKEABLE_LOCK_OUT && utxo.locktime === '0') + ) { const txidBuf = utils.cb58Decode(utxo.txid); const amt: BN = new BN(utxo.amount); const outputidx = utils.outputidxNumberToBuffer(utxo.outputidx); diff --git a/modules/sdk-coin-avaxp/src/lib/iface.ts b/modules/sdk-coin-avaxp/src/lib/iface.ts index 2759559dc9..8bac4adf66 100644 --- a/modules/sdk-coin-avaxp/src/lib/iface.ts +++ b/modules/sdk-coin-avaxp/src/lib/iface.ts @@ -50,6 +50,7 @@ export interface TxData { */ export type DecodedUtxoObj = { outputID: number; + locktime?: string; amount: string; txid: string; outputidx: string; @@ -61,9 +62,14 @@ export type DecodedUtxoObj = { /** * TypeId value for SECP256K1 Transfer Output * - * {@link https://docs.avax.network/specs/platform-transaction-serialization#secp256k1-transfer-output-example } + * {@link https://build.avax.network/docs/api-reference/p-chain/txn-format#secp256k1-transfer-output } */ export const SECP256K1_Transfer_Output = 7; +/** + * TypeId value for Stakeable Lock Output + * {@link https://build.avax.network/docs/api-reference/p-chain/txn-format#stakeablelockout } + */ +export const SECP256K1_STAKEABLE_LOCK_OUT = 22; export const ADDRESS_SEPARATOR = '~'; export const INPUT_SEPARATOR = ':'; diff --git a/modules/sdk-coin-avaxp/src/lib/permissionlessValidatorTxBuilder.ts b/modules/sdk-coin-avaxp/src/lib/permissionlessValidatorTxBuilder.ts index a0c16fb00c..ec2ba76bb0 100644 --- a/modules/sdk-coin-avaxp/src/lib/permissionlessValidatorTxBuilder.ts +++ b/modules/sdk-coin-avaxp/src/lib/permissionlessValidatorTxBuilder.ts @@ -30,7 +30,7 @@ import { import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { Buffer as BufferAvax } from 'avalanche'; import BigNumber from 'bignumber.js'; -import { DecodedUtxoObj, SECP256K1_Transfer_Output, Tx } from './iface'; +import { DecodedUtxoObj, SECP256K1_STAKEABLE_LOCK_OUT, SECP256K1_Transfer_Output, Tx } from './iface'; import { KeyPair } from './keyPair'; import { Transaction } from './transaction'; import { TransactionBuilder } from './transactionBuilder'; @@ -291,9 +291,6 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { this.transaction._stakeAmount = permissionlessValidatorTx.stake[0].output.amount(); this.stakeAmount(this.transaction._stakeAmount); this.transaction._utxos = recoverUtxos(permissionlessValidatorTx.getInputs()); - // TODO(CR-1073): remove log - console.log('utxos: ', this.transaction._utxos); - console.log('fromAddresses: ', this.transaction.fromAddresses); return this; } @@ -338,8 +335,6 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { const bitgoAddresses = this.transaction._fromAddresses.map((b) => avaxUtils.format(this.transaction._network.alias, this.transaction._network.hrp, b) ); - // TODO(CR-1073): remove log - console.log(`bitgoAddress: ${bitgoAddresses}`); // if we are in OVC, none of the utxos will have addresses since they come from // deserialized inputs (which don't have addresses), not the IMS @@ -371,12 +366,12 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { utxo.addresses.forEach((a) => { bitgoIndexToOnChainIndex.set(bitgoAddresses.indexOf(a), utxo.addresses.indexOf(a)); }); - // TODO(CR-1073): remove log - console.log(`utxo.addresses: ${utxo.addresses}`); - console.log(`bitgoIndexToOnChainIndex: ${Array.from(bitgoIndexToOnChainIndex)}`); // in OVC, output.addressesIndex is defined correctly from the previous iteration - if (utxo.outputID === SECP256K1_Transfer_Output) { + if ( + utxo.outputID === SECP256K1_Transfer_Output || + (utxo.outputID === SECP256K1_STAKEABLE_LOCK_OUT && utxo.locktime === '0') + ) { const utxoAmount = BigInt(utxo.amount); // either user (0) or recovery (2) // On regular mode: [user, bitgo] (i.e. [0, 1]) @@ -400,8 +395,6 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { new BigIntPr(utxoAmount), new Input([...addressesIndex].sort().map((num) => new Int(num))) ); - // TODO(CR-1073): remove log - console.log(`using addressesIndex sorted: ${[...addressesIndex].sort()}`); const input = new avaxSerial.TransferableInput(utxoId, assetId, transferInputs); utxos.push(new Utxo(utxoId, assetId, transferInputs)); @@ -413,12 +406,7 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { // For the user/backup signature we store the address that matches the key // if bitgo address comes before < user/backup address - // TODO(CR-1073): remove log - console.log(`bitgo index on chain: ${utxo.addressesIndex[bitgoIndex]}`); - console.log(`user Or Backup Index: ${utxo.addressesIndex[userOrBackupIndex]}`); if (utxo.addressesIndex[bitgoIndex] < utxo.addressesIndex[userOrBackupIndex]) { - // TODO(CR-1073): remove log - console.log(`user or backup credentials after bitgo`); credentials.push( new Credential([ utils.createNewSig(BufferAvax.from('').toString('hex')), @@ -428,8 +416,6 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { ]) ); } else { - // TODO(CR-1073): remove log - console.log(`user or backup credentials before bitgo`); credentials.push( new Credential([ utils.createNewSig( @@ -440,7 +426,6 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { ); } } else { - // TODO(CR-1073): verify this else case for OVC credentials.push( new Credential( addressesIndex.map((i) => @@ -449,9 +434,6 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { ) ); } - } else { - // TODO(CR-1073): remove log - console.log(`reusing credentials from transaction`); } } }); @@ -542,9 +524,8 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder { .map((a) => Address.fromBytes(a)[0]) ); - // TODO(CR-1073): check this value - // Shares 10,000 times percentage of reward taken from delegators - // https://docs.avax.network/reference/avalanchego/p-chain/txn-format#unsigned-add-validator-tx + // Shares 10,000 times percentage of reward taken from delegators + // https://docs.avax.network/reference/avalanchego/p-chain/txn-format#unsigned-add-validator-tx const shares = new Int(1e4 * 2); const addressMaps = [...this.transaction._fromAddresses] diff --git a/modules/sdk-coin-avaxp/src/lib/transactionBuilder.ts b/modules/sdk-coin-avaxp/src/lib/transactionBuilder.ts index 4da12bc1fa..6a209399fe 100644 --- a/modules/sdk-coin-avaxp/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-avaxp/src/lib/transactionBuilder.ts @@ -131,21 +131,12 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { return this; } - // TODO(CR-1073): - // Implement: - // buildImplementation - // signImplementation - // get transaction - // set transaction - // validateRawTransaction - /** @inheritdoc */ protected fromImplementation(rawTransaction: string): Transaction { const [tx] = pvmSerial.AddPermissionlessValidatorTx.fromBytes( Buffer.from(rawTransaction, 'hex'), avmSerial.getAVMManager().getDefaultCodec() ); - // TODO(CR-1073): check if initBuilder can only use UnsignedTx and pvmSerial.BaseTx is not required this.initBuilder(tx); return this._transaction; } diff --git a/modules/sdk-coin-avaxp/src/lib/utxoEngine.ts b/modules/sdk-coin-avaxp/src/lib/utxoEngine.ts index 7f7579c741..fb274e6788 100644 --- a/modules/sdk-coin-avaxp/src/lib/utxoEngine.ts +++ b/modules/sdk-coin-avaxp/src/lib/utxoEngine.ts @@ -1,4 +1,4 @@ -import { DecodedUtxoObj, SECP256K1_Transfer_Output } from './iface'; +import { DecodedUtxoObj, SECP256K1_STAKEABLE_LOCK_OUT, SECP256K1_Transfer_Output } from './iface'; import { BN, Buffer as BufferAvax } from 'avalanche'; import { Signature } from 'avalanche/dist/common'; import utils from './utils'; @@ -98,7 +98,12 @@ export function utxoToInput( let currentTotal: BN = new BN(0); const inputs = utxos - .filter((utxo) => utxo && utxo.outputID === SECP256K1_Transfer_Output) + .filter( + (utxo) => + utxo && + (utxo.outputID === SECP256K1_Transfer_Output || + (utxo.outputID === SECP256K1_STAKEABLE_LOCK_OUT && utxo.locktime === '0')) + ) .map((utxo) => { // validate the utxos const utxoAddresses: BufferAvax[] = utxo.addresses.map((a) => utils.parseAddress(a)); diff --git a/modules/sdk-coin-avaxp/test/resources/avaxp.ts b/modules/sdk-coin-avaxp/test/resources/avaxp.ts index a1235a86fe..b74b96b28a 100644 --- a/modules/sdk-coin-avaxp/test/resources/avaxp.ts +++ b/modules/sdk-coin-avaxp/test/resources/avaxp.ts @@ -622,7 +622,7 @@ export const BUILD_AND_SIGN_ADD_PERMISSIONLESS_VALIDATOR_SAMPLE = { '0xa94d6182edbd953516b262f17565a65d98f5741549cd70d2423abff750bb4b8d982d482376b189142ff8aa4705615fee14be6174610860e9c003aa4aeaa613b1732abf3cd0c9c42fa5856345644068c0d1f9fa1d9af32e20b14fca02983260bc', utxos: [ { - outputID: 7, + outputID: 22, amount: '98000000', txid: 's92SjoZQemgG97HocX9GgyFy6ZKmapgcgqQ3y5J2uwP3qWBUy', threshold: 2,