From f1c06bc6bf0f448fbd1c2dbcd5062e7e6f012c83 Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Thu, 16 Oct 2025 11:52:20 -0400 Subject: [PATCH] feat(sdk-coin-sol): big endian support for verifyTransaction TICKET: WP-6284 --- modules/sdk-coin-sol/src/sol.ts | 48 ++++++++++++++- modules/sdk-coin-sol/test/unit/sol.ts | 89 ++++++++++++++++++++++++++- 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/modules/sdk-coin-sol/src/sol.ts b/modules/sdk-coin-sol/src/sol.ts index ec39cacc00..525dd61d4b 100644 --- a/modules/sdk-coin-sol/src/sol.ts +++ b/modules/sdk-coin-sol/src/sol.ts @@ -178,6 +178,41 @@ export interface SolConsolidationRecoveryOptions extends MPCConsolidationRecover const HEX_REGEX = /^[0-9a-fA-F]+$/; const BLIND_SIGNING_TX_TYPES_TO_CHECK = { enabletoken: 'AssociatedTokenAccountInitialization' }; +/** + * Get amount string corrected for architecture-specific endianness issues. + * + * On s390x (big-endian) architecture, the Solana transaction parser (via @solana/web3.js) + * incorrectly reads little-endian u64 amounts as big-endian, resulting in corrupted values. + * + * This function corrects all amounts on s390x by swapping byte order to undo + * the incorrect byte order that happened during transaction parsing. + * + * @param amount - The amount to check and potentially fix + * @returns The corrected amount as a string + */ +export function getAmountBasedOnEndianness(amount: string | number): string { + const amountStr = String(amount); + + // Only s390x architecture has this endianness issue + const isS390x = process.arch === 's390x'; + if (!isS390x) { + return amountStr; + } + + try { + const amountBN = BigInt(amountStr); + // On s390x, the parser ALWAYS reads u64 as big-endian when it's actually little-endian + // So we ALWAYS need to swap bytes to get the correct value + const buf = Buffer.alloc(8); + buf.writeBigUInt64BE(amountBN, 0); + const fixed = buf.readBigUInt64LE(0); + return fixed.toString(); + } catch (e) { + // If conversion fails, return original value + return amountStr; + } +} + export class Sol extends BaseCoin { protected readonly _staticsCoin: Readonly; @@ -371,8 +406,12 @@ export class Sol extends BaseCoin { const recipientFromTx = filteredOutputs[index]; // This address should be an ATA // Compare the BigNumber values because amount is (string | number) - const userAmount = new BigNumber(recipientFromUser.amount); - const txAmount = new BigNumber(recipientFromTx.amount); + // Apply s390x endianness fix if needed + const userAmountStr = String(recipientFromUser.amount); + const txAmountStr = getAmountBasedOnEndianness(recipientFromTx.amount); + + const userAmount = new BigNumber(userAmountStr); + const txAmount = new BigNumber(txAmountStr); if (!userAmount.isEqualTo(txAmount)) { return false; } @@ -459,10 +498,13 @@ export class Sol extends BaseCoin { const explainedTxTotal: Record = {}; for (const output of explainedTx.outputs) { + // Apply s390x endianness fix to output amounts before summing + const outputAmountStr = getAmountBasedOnEndianness(output.amount); + // total output amount based on each token const assetName = output.tokenName || this.getChain(); const amount = explainedTxTotal[assetName] || new BigNumber(0); - explainedTxTotal[assetName] = amount.plus(output.amount); + explainedTxTotal[assetName] = amount.plus(outputAmountStr); } if (!_.isEqual(explainedTxTotal, totalAmount)) { diff --git a/modules/sdk-coin-sol/test/unit/sol.ts b/modules/sdk-coin-sol/test/unit/sol.ts index 01b6a39062..61133e585c 100644 --- a/modules/sdk-coin-sol/test/unit/sol.ts +++ b/modules/sdk-coin-sol/test/unit/sol.ts @@ -24,7 +24,7 @@ import { } from '@bitgo/sdk-core'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { coins } from '@bitgo/statics'; -import { KeyPair, Sol, SolVerifyTransactionOptions, Tsol } from '../../src'; +import { getAmountBasedOnEndianness, KeyPair, Sol, SolVerifyTransactionOptions, Tsol } from '../../src'; import { Transaction } from '../../src/lib'; import { AtaInit, InstructionParams, TokenTransfer } from '../../src/lib/iface'; import { getAssociatedTokenAccountAddress } from '../../src/lib/utils'; @@ -601,6 +601,93 @@ describe('SOL:', function () { }); }); + describe('getAmountBasedOnEndianness', () => { + let originalArch: string; + + beforeEach(() => { + originalArch = process.arch; + }); + + afterEach(() => { + Object.defineProperty(process, 'arch', { + value: originalArch, + writable: true, + configurable: true, + }); + }); + + it('should return amount unchanged on non-s390x architectures', function () { + Object.defineProperty(process, 'arch', { + value: 'x64', + writable: true, + configurable: true, + }); + + getAmountBasedOnEndianness('300000').should.equal('300000'); + getAmountBasedOnEndianness('10000').should.equal('10000'); + getAmountBasedOnEndianness('504403158265495552').should.equal('504403158265495552'); + }); + + it('should byte-swap small amounts on s390x (small becomes huge)', function () { + Object.defineProperty(process, 'arch', { + value: 's390x', + writable: true, + configurable: true, + }); + + // Small amount 10,000 (0x2710) swaps to 1,163,899,028,698,562,560 + getAmountBasedOnEndianness('10000').should.equal('1163899028698562560'); + }); + + it('should byte-swap large amounts on s390x (large becomes tiny)', function () { + Object.defineProperty(process, 'arch', { + value: 's390x', + writable: true, + configurable: true, + }); + + // Large amount 504,403,158,265,495,552 (0x0700000000000000) swaps to 7 + getAmountBasedOnEndianness('504403158265495552').should.equal('7'); + }); + + it('should handle numeric input', function () { + Object.defineProperty(process, 'arch', { + value: 's390x', + writable: true, + configurable: true, + }); + + // Should work with numbers, not just strings + getAmountBasedOnEndianness(10000).should.equal('1163899028698562560'); + }); + + it('should handle standard transaction amounts on s390x', function () { + Object.defineProperty(process, 'arch', { + value: 's390x', + writable: true, + configurable: true, + }); + + // Standard amount 300,000 (0x493E0) swaps to large value + const result = getAmountBasedOnEndianness('300000'); + // Verify it's different (swapped) + result.should.not.equal('300000'); + // Verify swapping back gives original + getAmountBasedOnEndianness(result).should.equal('300000'); + }); + + it('should handle invalid input gracefully', function () { + Object.defineProperty(process, 'arch', { + value: 's390x', + writable: true, + configurable: true, + }); + + // Invalid BigInt input should return original string + getAmountBasedOnEndianness('not-a-number').should.equal('not-a-number'); + }); + }); + it('should accept valid address', function () { goodAddresses.forEach((addr) => { basecoin.isValidAddress(addr).should.equal(true);