Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions modules/abstract-substrate/src/abstractSubstrateCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
RecoveryTxRequest,
SignedTransaction,
TssVerifyAddressOptions,
TransactionPrebuild,
TxIntentMismatchRecipientError,
UnexpectedAddressError,
verifyEddsaTssWalletAddress,
Expand Down Expand Up @@ -147,6 +148,15 @@ export class SubstrateCoin extends BaseCoin {
}

const factory = this.getBuilder();
// Use the live chain material embedded in the prebuild (if present) so the factory can decode
// transactions built against a chain spec version newer than the SDK's hardcoded metadata.
// Without this, the factory falls back to its hardcoded material and misidentifies the call
// pallet (e.g. reads a Balances transfer as treasury.disbursement after a chain upgrade).
const prebuildMaterial = (txPrebuild as TransactionPrebuild & { coinSpecific?: { material?: Material } })
.coinSpecific?.material;
if (prebuildMaterial && typeof factory.material === 'function') {
factory.material(prebuildMaterial);
}
const txBuilder = factory.from(txPrebuild.txHex) as unknown as NativeTransferBuilder;
const txTo: string = txBuilder['_to'];
const txAmount: string = txBuilder['_amount'];
Expand Down
28 changes: 26 additions & 2 deletions modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { UnbondBuilder } from './unbondBuilder';
import { WithdrawUnbondedBuilder } from './withdrawUnbondedBuilder';
import utils from './utils';
import { Interface, SingletonRegistry, TransactionBuilder } from './';
import { TxMethod, BatchCallObject, MethodNames, AddAndAffirmWithMediatorsArgs } from './iface';
import {
TxMethod,
BatchCallObject,
MethodNames,
AddAndAffirmWithMediatorsArgs,
V8AddAndAffirmWithMediatorsArgs,
} from './iface';
import { Transaction as BaseTransaction } from '@bitgo/abstract-substrate';
import { Transaction as PolyxTransaction } from './transaction';
import { PreApproveAssetBuilder } from './preApproveAssetBuilder';
Expand Down Expand Up @@ -257,7 +263,25 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
}

const methodName = decodedTxn.method?.name;
if (methodName === 'batchAll') {
if (methodName === Interface.MethodNames.TransferWithMemo) {
const args = decodedTxn.method.args as Interface.TransferWithMemoArgs;
if (utils.isNewMemoEncoding(args.memo)) {
return this.getV8HexTransferBuilder();
}
return this.getV8TransferBuilder();
} else if (methodName === MethodNames.AddAndAffirmWithMediators) {
const args = decodedTxn.method.args as AddAndAffirmWithMediatorsArgs | V8AddAndAffirmWithMediatorsArgs;
if (utils.isNewMemoEncoding(args.instructionMemo)) {
return this.getV8HexTokenTransferBuilder();
}
return this.getV8TokenTransferBuilder();
} else if (methodName === MethodNames.RegisterDidWithCDD) {
return this.getV8RegisterDidWithCDDBuilder();
} else if (methodName === MethodNames.RegisterDid) {
return this.getV8RegisterDidBuilder();
} else if (methodName === MethodNames.PreApproveAsset) {
return this.getV8PreApproveAssetBuilder();
} else if (methodName === 'batchAll') {
const args = decodedTxn.method.args as { calls?: BatchCallObject[] };

if (args.calls && args.calls.length === 2) {
Expand Down
65 changes: 64 additions & 1 deletion modules/sdk-coin-polyx/test/unit/polyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { coins } from '@bitgo/statics';
import * as sinon from 'sinon';
import * as testData from '../resources/wrwUsers';
import { afterEach } from 'mocha';
import { genesisHash, specVersion, txVersion, rawTx } from '../resources';
import { genesisHash, specVersion, txVersion, rawTx, accounts, mockTssSignature } from '../resources';
import { TxIntentMismatchRecipientError } from '@bitgo/sdk-core';
import { V8TransferBuilder } from '../../src/lib';
import { testnetV8Material } from '../../src/resources';

describe('Polyx:', function () {
let bitgo: TestBitGoAPI;
Expand Down Expand Up @@ -272,5 +274,66 @@ describe('Polyx:', function () {
.should.be.rejectedWith('missing recipients in txParams');
});
});

describe('v8 transfer with coinSpecific.material', function () {
const v8Amount = '1000000';
const v8Receiver = accounts.account2;
const v8Sender = accounts.account1;
let v8SignedTxHex: string;

before(async function () {
// Build a signed v8 transfer tx using testnetV8Material.
// The server embeds this material in coinSpecific during prebuild so that
// verifyTransaction can decode txHex built against a chain spec newer than
// the SDK's hardcoded v7 material (CECHO-1471).
const builder = new V8TransferBuilder(coins.get('tpolyx'))
.amount(v8Amount)
.to({ address: v8Receiver.address })
.sender({ address: v8Sender.address })
.memo('0')
.validity({ firstValid: 3933, maxDuration: 64 })
.referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d')
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 1 })
.fee({ amount: 0, type: 'tip' });
builder.addSignature({ pub: v8Sender.publicKey }, Buffer.from(mockTssSignature, 'hex'));
const tx = await builder.build();
v8SignedTxHex = tx.toBroadcastFormat();
});

it('verifies a v8 transfer when coinSpecific.material is provided', async function () {
const result = await baseCoin.verifyTransaction({
txPrebuild: {
txHex: v8SignedTxHex,
coinSpecific: { material: testnetV8Material },
},
txParams: { recipients: [{ address: v8Receiver.address, amount: v8Amount }] },
});
result.should.be.true();
});

it('throws TxIntentMismatchRecipientError for address mismatch on v8 transfer', async function () {
await baseCoin
.verifyTransaction({
txPrebuild: {
txHex: v8SignedTxHex,
coinSpecific: { material: testnetV8Material },
},
txParams: { recipients: [{ address: wrongAddress, amount: v8Amount }] },
})
.should.be.rejectedWith(TxIntentMismatchRecipientError);
});

it('throws TxIntentMismatchRecipientError for amount mismatch on v8 transfer', async function () {
await baseCoin
.verifyTransaction({
txPrebuild: {
txHex: v8SignedTxHex,
coinSpecific: { material: testnetV8Material },
},
txParams: { recipients: [{ address: v8Receiver.address, amount: '1' }] },
})
.should.be.rejectedWith(TxIntentMismatchRecipientError);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { coins } from '@bitgo/statics';
import should from 'should';
import { TransactionBuilderFactory, TransferBuilder } from '../../../src/lib';
import { TransactionBuilderFactory, TransferBuilder, V8TransferBuilder, V8HexTransferBuilder } from '../../../src/lib';
import { Interface } from '../../../src';
import { rawTx, accounts } from '../../resources';
import * as materialData from '../../resources/materialData.json';
import { buildTestConfig } from './base';

describe('Tao Transaction Builder Factory', function () {
const sender = accounts.account1;
Expand Down Expand Up @@ -41,4 +42,62 @@ describe('Tao Transaction Builder Factory', function () {
});
});
});

describe('tryGetV8Builder routing', function () {
const receiver = accounts.account2;

let v8TransferTxHex: string;
let v8HexTransferTxHex: string;

before(async function () {
const config = buildTestConfig();

// Build an unsigned v8 transferWithMemo tx (plain-string memo, not 0x-prefixed)
const v8Tx = await new V8TransferBuilder(config)
.amount('1000000')
.to({ address: receiver.address })
.sender({ address: sender.address })
.memo('0')
.validity({ firstValid: 3933, maxDuration: 64 })
.referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d')
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 1 })
.fee({ amount: 0, type: 'tip' })
.build();
v8TransferTxHex = v8Tx.toBroadcastFormat();

// Build an unsigned v8 hex-memo transferWithMemo tx
const v8HexTx = await new V8HexTransferBuilder(config)
.amount('1000000')
.to({ address: receiver.address })
.sender({ address: sender.address })
.memo('56594')
.validity({ firstValid: 3933, maxDuration: 64 })
.referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d')
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 1 })
.fee({ amount: 0, type: 'tip' })
.build();
v8HexTransferTxHex = v8HexTx.toBroadcastFormat();
});

it('routes a v8 transferWithMemo tx to V8TransferBuilder via fallback', function () {
// A v8 txHex encodes transferWithMemo at call index 0x28 (Balances pallet, call 40),
// which does not exist in the v7 SDK metadata. The factory falls back to tryGetV8Builder,
// decodes against v8 metadata, and returns the correct builder.
const factoryInst = new TransactionBuilderFactory(buildTestConfig());
Comment thread
nvrakesh06 marked this conversation as resolved.
const builder = factoryInst.from(v8TransferTxHex);
should.ok(builder instanceof V8TransferBuilder, 'expected V8TransferBuilder from tryGetV8Builder fallback');
});

it('routes a v8 hex-memo transferWithMemo tx to V8HexTransferBuilder via fallback', function () {
const factoryInst = new TransactionBuilderFactory(buildTestConfig());
const builder = factoryInst.from(v8HexTransferTxHex);
should.ok(builder instanceof V8HexTransferBuilder, 'expected V8HexTransferBuilder from tryGetV8Builder fallback');
});

it('routes a v7 transferWithMemo tx to TransferBuilder directly', function () {
const factoryInst = new TransactionBuilderFactory(buildTestConfig());
const builder = factoryInst.from(rawTx.transfer.signed);
should.ok(builder instanceof TransferBuilder, 'expected TransferBuilder for v7 txHex');
});
});
});
Loading