diff --git a/modules/sdk-coin-sol/src/lib/constants.ts b/modules/sdk-coin-sol/src/lib/constants.ts index a8f035bb5d..ddf11804c1 100644 --- a/modules/sdk-coin-sol/src/lib/constants.ts +++ b/modules/sdk-coin-sol/src/lib/constants.ts @@ -105,3 +105,6 @@ export const ataInitInstructionIndexes = { InitializeAssociatedTokenAccount: 0, Memo: 1, } as const; + +export const nonceAdvanceInstruction = 'AdvanceNonceAccount'; +export const validInstructionData = '0a00000001000000'; diff --git a/modules/sdk-coin-sol/src/lib/iface.ts b/modules/sdk-coin-sol/src/lib/iface.ts index c8d8be5f23..2995e9aaa2 100644 --- a/modules/sdk-coin-sol/src/lib/iface.ts +++ b/modules/sdk-coin-sol/src/lib/iface.ts @@ -88,7 +88,13 @@ export interface StakingWithdraw { export interface StakingAuthorize { type: InstructionBuilderTypes.StakingAuthorize; - params: { stakingAddress: string; oldAuthorizeAddress; newAuthorizeAddress: string; newWithdrawAddress: string }; + params: { + stakingAddress: string; + oldAuthorizeAddress; + newAuthorizeAddress: string; + newWithdrawAddress?: string; + custodianAddress?: string; + }; } export interface AtaInit { @@ -103,12 +109,20 @@ export type ValidInstructionTypes = | 'InitializeAssociatedTokenAccount' | 'TokenTransfer'; +export type StakingAuthorizeParams = { + stakingAddress: string; + oldWithdrawAddress: string; + newWithdrawAddress: string; + custodianAddress?: string; +}; + export interface TransactionExplanation extends BaseTransactionExplanation { type: string; blockhash: Blockhash; // only populated if blockhash is from a nonce account durableNonce?: DurableNonceParams; memo?: string; + stakingAuthorize?: StakingAuthorizeParams; } export class TokenAssociateRecipient { diff --git a/modules/sdk-coin-sol/src/lib/index.ts b/modules/sdk-coin-sol/src/lib/index.ts index 0ff5d7526f..48eddd0f20 100644 --- a/modules/sdk-coin-sol/src/lib/index.ts +++ b/modules/sdk-coin-sol/src/lib/index.ts @@ -14,4 +14,5 @@ export { StakingActivateBuilder } from './stakingActivateBuilder'; export { StakingDeactivateBuilder } from './stakingDeactivateBuilder'; export { StakingWithdrawBuilder } from './stakingWithdrawBuilder'; export { StakingAuthorizeBuilder } from './stakingAuthorizeBuilder'; +export { StakingRawMsgAuthorizeBuilder } from './stakingRawMsgAuthorizeBuilder'; export { Utils, Interface }; diff --git a/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts b/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts index 08d5297613..1feb519735 100644 --- a/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts +++ b/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts @@ -59,6 +59,8 @@ export function instructionParamsFactory( return parseAtaInitInstructions(instructions); case TransactionType.StakingAuthorize: return parseStakingAuthorizeInstructions(instructions); + case TransactionType.StakingAuthorizeRaw: + return parseStakingAuthorizeRawInstructions(instructions); default: throw new NotSupported('Invalid transaction, transaction type not supported: ' + type); } @@ -563,6 +565,39 @@ function parseStakingAuthorizeInstructions( return instructionData; } +/** + * Parses Solana instructions to authorized staking account params + * Only supports Nonce, Authorize instructions + * + * @param {TransactionInstruction[]} instructions - an array of supported Solana instructions + * @returns {InstructionParams[]} An array containing instruction params for staking authorize tx + */ +function parseStakingAuthorizeRawInstructions(instructions: TransactionInstruction[]): Array { + const instructionData: Array = []; + assert(instructions.length === 2, 'Invalid number of instructions'); + const advanceNonceInstruction = SystemInstruction.decodeNonceAdvance(instructions[0]); + const nonce: Nonce = { + type: InstructionBuilderTypes.NonceAdvance, + params: { + walletNonceAddress: advanceNonceInstruction.noncePubkey.toString(), + authWalletAddress: advanceNonceInstruction.authorizedPubkey.toString(), + }, + }; + instructionData.push(nonce); + const authorize = instructions[1]; + assert(authorize.keys.length === 5, 'Invalid number of keys in authorize instruction'); + instructionData.push({ + type: InstructionBuilderTypes.StakingAuthorize, + params: { + stakingAddress: authorize.keys[0].pubkey.toString(), + oldAuthorizeAddress: authorize.keys[2].pubkey.toString(), + newAuthorizeAddress: authorize.keys[3].pubkey.toString(), + custodianAddress: authorize.keys[4].pubkey.toString(), + }, + }); + return instructionData; +} + function findTokenName(mintAddress: string): string { let token: string | undefined; diff --git a/modules/sdk-coin-sol/src/lib/stakingRawMsgAuthorizeBuilder.ts b/modules/sdk-coin-sol/src/lib/stakingRawMsgAuthorizeBuilder.ts new file mode 100644 index 0000000000..ce1355e33a --- /dev/null +++ b/modules/sdk-coin-sol/src/lib/stakingRawMsgAuthorizeBuilder.ts @@ -0,0 +1,137 @@ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { + BaseAddress, + BaseKey, + BaseTransaction, + BaseTransactionBuilder, + NotSupported, + TransactionType, +} from '@bitgo/sdk-core'; +import { Transaction } from './transaction'; +import { + Transaction as SOLTransaction, + Message as SOLMessage, + SystemProgram, + SystemInstruction, + StakeProgram, +} from '@solana/web3.js'; + +import assert from 'assert'; +import BigNumber from 'bignumber.js'; +import { nonceAdvanceInstruction, validInstructionData } from './constants'; + +export class StakingRawMsgAuthorizeBuilder extends BaseTransactionBuilder { + protected _transaction: Transaction; + protected _transactionMessage: string; + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._transaction = new Transaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.StakingAuthorizeRaw; + } + + /** @inheritdoc */ + initBuilder(tx: Transaction): void { + if (this.validateTransaction(tx)) { + this.transactionMessage(tx.solTransaction.serializeMessage().toString('base64')); + } + } + + /** + * The raw message generated by Solana CLI. + * + * @param {string} msg msg generated by 'solana stake-authorize-check. + * @returns {StakeBuilder} This staking builder. + * + */ + transactionMessage(msg: string): this { + this.validateMessage(msg); + this._transactionMessage = msg; + return this; + } + + /** @inheritdoc */ + protected async buildImplementation(): Promise { + assert(this._transactionMessage, 'missing transaction message'); + + this.validateMessage(this._transactionMessage); + this.transaction.solTransaction = SOLTransaction.populate( + SOLMessage.from(Buffer.from(this._transactionMessage, 'base64')), + [] + ); + this.transaction.setTransactionType(this.transactionType); + return this.transaction; + } + + validateTransaction(tx: Transaction): boolean { + return this.validateMessage(tx.solTransaction.serializeMessage().toString('base64')); + } + + async build(): Promise { + return this.buildImplementation(); + } + + protected validateMessage(msg: string): boolean { + const tx = SOLTransaction.populate(SOLMessage.from(Buffer.from(msg, 'base64')), []); + const instructions = tx.instructions; + if (instructions.length !== 2) { + throw new Error(`Invalid transaction, expected 2 instruction, got ${instructions.length}`); + } + for (const instruction of instructions) { + switch (instruction.programId.toString()) { + case SystemProgram.programId.toString(): + const instructionName = SystemInstruction.decodeInstructionType(instruction); + if (instructionName !== nonceAdvanceInstruction) { + throw new Error(`Invalid system instruction : ${instructionName}`); + } + break; + case StakeProgram.programId.toString(): + const data = instruction.data.toString('hex'); + if (data !== validInstructionData) { + throw new Error(`Invalid staking instruction data: ${data}`); + } + break; + default: + throw new Error( + `Invalid transaction, instruction program id not supported: ${instruction.programId.toString()}` + ); + } + } + return true; + } + + protected fromImplementation(rawTransaction: string): Transaction { + const tx = new Transaction(this._coinConfig); + tx.fromRawTransaction(rawTransaction); + this.initBuilder(tx); + return this.transaction; + } + + protected signImplementation(key: BaseKey): BaseTransaction { + throw new NotSupported('Method not supported on this builder'); + } + + protected get transaction(): Transaction { + return this._transaction; + } + + validateAddress(address: BaseAddress, addressFormat?: string): void { + throw new NotSupported('Method not supported on this builder'); + } + + validateKey(key: BaseKey): void { + throw new NotSupported('Method not supported on this builder'); + } + + validateRawTransaction(rawTransaction: string): void { + const tx = new Transaction(this._coinConfig); + tx.fromRawTransaction(rawTransaction); + this.validateTransaction(tx); + } + + validateValue(value: BigNumber): void { + throw new NotSupported('Method not supported on this builder'); + } +} diff --git a/modules/sdk-coin-sol/src/lib/transaction.ts b/modules/sdk-coin-sol/src/lib/transaction.ts index e617e8b14e..ae80f5dce1 100644 --- a/modules/sdk-coin-sol/src/lib/transaction.ts +++ b/modules/sdk-coin-sol/src/lib/transaction.ts @@ -15,6 +15,7 @@ import { Memo, Nonce, StakingActivate, + StakingAuthorizeParams, StakingWithdraw, TokenTransfer, TransactionExplanation, @@ -23,10 +24,17 @@ import { WalletInit, } from './iface'; import base58 from 'bs58'; -import { getInstructionType, getTransactionType, isValidRawTransaction, requiresAllSignatures } from './utils'; +import { + getInstructionType, + getTransactionType, + isValidRawTransaction, + requiresAllSignatures, + validateRawMsgInstruction, +} from './utils'; import { KeyPair } from '.'; import { instructionParamsFactory } from './instructionParamsFactory'; -import { InstructionBuilderTypes, ValidInstructionTypesEnum, UNAVAILABLE_TEXT } from './constants'; +import { InstructionBuilderTypes, UNAVAILABLE_TEXT, ValidInstructionTypesEnum } from './constants'; + export class Transaction extends BaseTransaction { protected _solTransaction: SolTransaction; private _lamportsPerSignature: number | undefined; @@ -194,8 +202,13 @@ export class Transaction extends BaseTransaction { case TransactionType.StakingAuthorize: this.setTransactionType(TransactionType.StakingAuthorize); break; + case TransactionType.StakingAuthorizeRaw: + this.setTransactionType(TransactionType.StakingAuthorizeRaw); + break; + } + if (transactionType !== TransactionType.StakingAuthorizeRaw) { + this.loadInputsAndOutputs(); } - this.loadInputsAndOutputs(); } catch (e) { throw e; } @@ -216,6 +229,16 @@ export class Transaction extends BaseTransaction { }; } + if (this._type) { + const instrunctionData = instructionParamsFactory(this._type, this._solTransaction.instructions); + if ( + !durableNonce && + instrunctionData.length > 1 && + instrunctionData[0].type === InstructionBuilderTypes.NonceAdvance + ) { + durableNonce = instrunctionData[0].params; + } + } const result: TxData = { id: this._solTransaction.signature ? this.id : undefined, feePayer: this._solTransaction.feePayer?.toString(), @@ -336,6 +359,9 @@ export class Transaction extends BaseTransaction { /** @inheritDoc */ explainTransaction(): TransactionExplanation { + if (validateRawMsgInstruction(this._solTransaction.instructions)) { + return this.explainRawMsgAuthorizeTransaction(); + } const decodedInstructions = instructionParamsFactory(this._type, this._solTransaction.instructions); let memo: string | undefined = undefined; @@ -461,4 +487,47 @@ export class Transaction extends BaseTransaction { durableNonce: durableNonce, }; } + + private explainRawMsgAuthorizeTransaction(): TransactionExplanation { + const { instructions } = this._solTransaction; + const nonceInstruction = SystemInstruction.decodeNonceAdvance(instructions[0]); + const durableNonce = { + walletNonceAddress: nonceInstruction.noncePubkey.toString(), + authWalletAddress: nonceInstruction.authorizedPubkey.toString(), + }; + const stakingAuthorizeParams: StakingAuthorizeParams = { + stakingAddress: instructions[1].keys[0].pubkey.toString(), + oldWithdrawAddress: instructions[1].keys[2].pubkey.toString(), + newWithdrawAddress: instructions[1].keys[3].pubkey.toString(), + custodianAddress: instructions[1].keys[4].pubkey.toString(), + }; + const feeString = this.calculateFee(); + return { + displayOrder: [ + 'id', + 'type', + 'blockhash', + 'durableNonce', + 'outputAmount', + 'changeAmount', + 'outputs', + 'changeOutputs', + 'fee', + 'memo', + ], + id: this.id, + type: TransactionType[this.type].toString(), + changeOutputs: [], + changeAmount: '0', + outputAmount: 0, + outputs: [], + fee: { + fee: feeString, + feeRate: this.lamportsPerSignature, + }, + blockhash: this.getNonce(), + durableNonce: durableNonce, + stakingAuthorize: stakingAuthorizeParams, + }; + } } diff --git a/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts index 73cf053663..253ef0500e 100644 --- a/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts @@ -12,6 +12,7 @@ import { AtaInitializationBuilder } from './ataInitializationBuilder'; import { TokenTransferBuilder } from './tokenTransferBuilder'; import { TransferBuilderV2 } from './transferBuilderV2'; import { StakingAuthorizeBuilder } from './stakingAuthorizeBuilder'; +import { StakingRawMsgAuthorizeBuilder } from './stakingRawMsgAuthorizeBuilder'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { constructor(_coinConfig: Readonly) { @@ -23,7 +24,7 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { * * @param { string} raw - Encoded transaction in base64 string format */ - from(raw: string): TransactionBuilder { + from(raw: string): TransactionBuilder | StakingRawMsgAuthorizeBuilder { validateRawTransaction(raw); const tx = this.parseTransaction(raw); try { @@ -49,6 +50,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.getAtaInitializationBuilder(tx); case TransactionType.StakingAuthorize: return this.getStakingAuthorizeBuilder(tx); + case TransactionType.StakingAuthorizeRaw: + return this.getStakingRawMsgAuthorizeBuilder(tx); default: throw new InvalidTransactionError('Invalid transaction'); } @@ -129,6 +132,20 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new StakingAuthorizeBuilder(this._coinConfig)); } + /** + * Returns the raw message builder to authorized staking account. + * + * @param {Transaction} tx - the transaction to be used to intialize the builder + * @returns {StakingWithdrawBuilder} - the initialized staking authorize builder + */ + getStakingRawMsgAuthorizeBuilder(tx?: Transaction): StakingRawMsgAuthorizeBuilder { + const builder = new StakingRawMsgAuthorizeBuilder(this._coinConfig); + if (tx) { + builder.initBuilder(tx); + } + return builder; + } + /** * Returns the builder to create a create associated token account transaction. */ diff --git a/modules/sdk-coin-sol/src/lib/utils.ts b/modules/sdk-coin-sol/src/lib/utils.ts index e1f9862451..907caa485e 100644 --- a/modules/sdk-coin-sol/src/lib/utils.ts +++ b/modules/sdk-coin-sol/src/lib/utils.ts @@ -23,6 +23,8 @@ import { VALID_SYSTEM_INSTRUCTION_TYPES, ValidInstructionTypesEnum, walletInitInstructionIndexes, + nonceAdvanceInstruction, + validInstructionData, } from './constants'; import { BuildTransactionError, @@ -252,6 +254,9 @@ export function matchTransactionTypeByInstructionsOrder( */ export function getTransactionType(transaction: SolTransaction): TransactionType { const { instructions } = transaction; + if (validateRawMsgInstruction(instructions)) { + return TransactionType.StakingAuthorizeRaw; + } validateIntructionTypes(instructions); for (const instruction of instructions) { const instructionType = getInstructionType(instruction); @@ -330,6 +335,27 @@ export function validateIntructionTypes(instructions: TransactionInstruction[]): } } +/** + * Validate solana instructions match raw msg authorize transaction + * + * @param {TransactionInstruction} instructions - a solana instruction + * @returns {boolean} true if the instructions match the raw msg authorize transaction + */ +export function validateRawMsgInstruction(instructions: TransactionInstruction[]): boolean { + // as web3.js cannot decode authorize instruction from CLI, we need to check it manually first + if (instructions.length === 2) { + const programId1 = instructions[0].programId.toString(); + const programId2 = instructions[1].programId.toString(); + if (programId1 === SystemProgram.programId.toString() && programId2 === StakeProgram.programId.toString()) { + const instructionName1 = SystemInstruction.decodeInstructionType(instructions[0]); + const data = instructions[1].data.toString('hex'); + if (instructionName1 === nonceAdvanceInstruction && data === validInstructionData) { + return true; + } + } + } + return false; +} /** * Check the raw transaction has a valid format in the blockchain context, throw otherwise. * diff --git a/modules/sdk-coin-sol/src/sol.ts b/modules/sdk-coin-sol/src/sol.ts index 736d1156dc..6616ad64de 100644 --- a/modules/sdk-coin-sol/src/sol.ts +++ b/modules/sdk-coin-sol/src/sol.ts @@ -31,7 +31,7 @@ import { EDDSAMethodTypes, EDDSAMethods, } from '@bitgo/sdk-core'; -import { KeyPair as SolKeyPair, Transaction, TransactionBuilderFactory } from './lib'; +import { KeyPair as SolKeyPair, Transaction, TransactionBuilder, TransactionBuilderFactory } from './lib'; import { getAssociatedTokenAccountAddress, getSolTokenFromTokenName, @@ -415,9 +415,13 @@ export class Sol extends BaseCoin { let rebuiltTransaction; try { - const transactionBuilder = factory.from(params.txBase64).fee({ amount: params.feeInfo.fee }); - if (params.tokenAccountRentExemptAmount) { - transactionBuilder.associatedTokenAccountRent(params.tokenAccountRentExemptAmount); + const transactionBuilder = factory.from(params.txBase64); + if (transactionBuilder instanceof TransactionBuilder) { + const txBuilder = transactionBuilder as TransactionBuilder; + txBuilder.fee({ amount: params.feeInfo.fee }); + if (params.tokenAccountRentExemptAmount) { + txBuilder.associatedTokenAccountRent(params.tokenAccountRentExemptAmount); + } } rebuiltTransaction = await transactionBuilder.build(); } catch (e) { diff --git a/modules/sdk-coin-sol/test/resources/sol.ts b/modules/sdk-coin-sol/test/resources/sol.ts index 87f326e99e..ee18372183 100644 --- a/modules/sdk-coin-sol/test/resources/sol.ts +++ b/modules/sdk-coin-sol/test/resources/sol.ts @@ -191,6 +191,11 @@ export const STAKING_AUTHORIZE_SIGNED_TX = export const STAKING_AUTHORIZE_UNSIGNED_TX = 'AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgEBBWTJ6tmqa2VEWs9PpSYIC89TuqrV4L25lXjJ/CM/nB31ReV5vPklPPaLR9/x+zo6XCwhusWyPAmuEqbgVWvwi0Fiey+6ASdh+bkZvPlMu0ydyAUdnwkymTFNOUkjMmi96Qan1RcYx3TJKFZjmGkdXraLXrijm0ttXHNVWyEAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAADjMtr5L6vs6LY/96RABeX9/Zr6FYdWthxalfkEs7jQgQIEAwIDASgBAAAAZMnq2aprZURaz0+lJggLz1O6qtXgvbmVeMn8Iz+cHfUAAAAABAQCAwEAKAEAAABkyerZqmtlRFrPT6UmCAvPU7qq1eC9uZV4yfwjP5wd9QEAAAA='; +export const STAKING_AUTHORIZE_RAW_MSG = + 'BAMECUjf8gIq6GG64d918PjCkhrbvJuB8eLKsJ3AF8/4Xvm/OJ3quPmDxHGaJQ8i7UEcK4Lxw2Wg5EztQBVfvCRAqWVYjjb7iYYF+oozdaIF15LTJa1vS7Bode1kTBYsG1/Q47ZyiKyuVSf+b4UInX8th+YndezOzAqlPM/X7i/xtupG/omTU133IVyCbT/ciqwAkSbp0+8PW5ILOyi530DuHv0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAah2BeRN1QqmDQ3vf4qerJVf1NcinhyK2ikncAAAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAAGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAB7+eIY2cl1LvdiFfz19uiPM6YH13OUv82VzFEZTlEo0AgUDBAgABAQAAAAGBQQHAgEDCAoAAAABAAAA'; + +export const STAKING_AUTHORIZE_RAW_MSG_TXN = + 'BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAwQJSN/yAiroYbrh33Xw+MKSGtu8m4Hx4sqwncAXz/he+b84neq4+YPEcZolDyLtQRwrgvHDZaDkTO1AFV+8JECpZViONvuJhgX6ijN1ogXXktMlrW9LsGh17WRMFiwbX9DjtnKIrK5VJ/5vhQidfy2H5id17M7MCqU8z9fuL/G26kb+iZNTXfchXIJtP9yKrACRJunT7w9bkgs7KLnfQO4e/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqHYF5E3VCqYNDe9/ip6slV/U1yKeHIraKSdwAAAAAAGp9UXGMd0yShWY5hpHV62i164o5tLbVxzVVshAAAAAAan1RcZLFaO4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAAHv54hjZyXUu92IV/PX26I8zpgfXc5S/zZXMURlOUSjQCBQMECAAEBAAAAAYFBAcCAQMICgAAAAEAAAA='; // #endregion staking export const WALLET_INIT_UNSIGNED_TX = diff --git a/modules/sdk-coin-sol/test/unit/transactionBuilder/staingRawMsgAuthorizeBuilder.ts b/modules/sdk-coin-sol/test/unit/transactionBuilder/staingRawMsgAuthorizeBuilder.ts new file mode 100644 index 0000000000..ffa5e61547 --- /dev/null +++ b/modules/sdk-coin-sol/test/unit/transactionBuilder/staingRawMsgAuthorizeBuilder.ts @@ -0,0 +1,105 @@ +import should from 'should'; +import * as testData from '../../resources/sol'; +import { getBuilderFactory } from '../getBuilderFactory'; +import { Utils, Transaction } from '../../../src'; + +describe('Sol Staking Raw Message Authorize Builder', () => { + const factory = getBuilderFactory('tsol'); + + it('should build a create staking authorization unsigned tx', async () => { + const txBuilder = factory.getStakingRawMsgAuthorizeBuilder(); + txBuilder.transactionMessage(testData.STAKING_AUTHORIZE_RAW_MSG); + const tx = await txBuilder.build(); + tx.inputs.length.should.equal(0); + tx.outputs.length.should.equal(0); + const rawTx = tx.toBroadcastFormat(); + should.equal(Utils.isValidRawTransaction(rawTx), true); + should.equal(rawTx, testData.STAKING_AUTHORIZE_RAW_MSG_TXN); + const explain = tx.explainTransaction(); + should.equal(explain.type, 'StakingAuthorizeRaw'); + should.equal(explain.blockhash, '35zHbmNtFNB9ADux97UbdY1bQSobfYR2SUmuCnqVfpUB'); + should.deepEqual(explain.durableNonce, { + walletNonceAddress: 'J8cECxcT6Q6H4fcQCvd4LbhmmSjsHL63kpJtrUcrF74Q', + authWalletAddress: '5uUQw7ZtTRYduT6MrsPQeGKAavRek2VzxAqgUxQE2szv', + }); + should.deepEqual(explain.stakingAuthorize, { + stakingAddress: 'J8cECxcT6Q6H4fcQCvd4LbhmmSjsHL63kpJtrUcrF74Q', + oldWithdrawAddress: '6xgesG4vajCYfAQpknodrarD49ZCnXGvYA4H1DLuGV7Y', + newWithdrawAddress: '4p1VdN6BngTAbWR7Q5JPpbB6dc4k4y8wn1knmmWEjc9i', + custodianAddress: 'DHCVjKy7kN6D6vM69nHcEeEeS685qtonFbiFNBW5bGiq', + }); + }); + + it('should build from an unsigned transaction', async () => { + const txBuilder = factory.from(testData.STAKING_AUTHORIZE_RAW_MSG_TXN); + const tx = await txBuilder.build(); + tx.inputs.length.should.equal(0); + tx.outputs.length.should.equal(0); + const rawTx = tx.toBroadcastFormat(); + const signable = tx.signablePayload.toString('base64'); + should.equal(signable, testData.STAKING_AUTHORIZE_RAW_MSG); + should.equal(Utils.isValidRawTransaction(rawTx), true); + should.equal(rawTx, testData.STAKING_AUTHORIZE_RAW_MSG_TXN); + }); + + it('should explain a transaction', async () => { + const txBuilder = factory.from(testData.STAKING_AUTHORIZE_RAW_MSG_TXN); + const tx = await txBuilder.build(); + const explain = tx.explainTransaction(); + should.equal(explain.type, 'StakingAuthorizeRaw'); + should.equal(explain.blockhash, '35zHbmNtFNB9ADux97UbdY1bQSobfYR2SUmuCnqVfpUB'); + should.deepEqual(explain.durableNonce, { + walletNonceAddress: 'J8cECxcT6Q6H4fcQCvd4LbhmmSjsHL63kpJtrUcrF74Q', + authWalletAddress: '5uUQw7ZtTRYduT6MrsPQeGKAavRek2VzxAqgUxQE2szv', + }); + should.deepEqual(explain.stakingAuthorize, { + stakingAddress: 'J8cECxcT6Q6H4fcQCvd4LbhmmSjsHL63kpJtrUcrF74Q', + oldWithdrawAddress: '6xgesG4vajCYfAQpknodrarD49ZCnXGvYA4H1DLuGV7Y', + newWithdrawAddress: '4p1VdN6BngTAbWR7Q5JPpbB6dc4k4y8wn1knmmWEjc9i', + custodianAddress: 'DHCVjKy7kN6D6vM69nHcEeEeS685qtonFbiFNBW5bGiq', + }); + }); + + it('should generate json from transaction', async () => { + const txBuilder = factory.from(testData.STAKING_AUTHORIZE_RAW_MSG_TXN); + const tx = await txBuilder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.durableNonce, { + walletNonceAddress: 'J8cECxcT6Q6H4fcQCvd4LbhmmSjsHL63kpJtrUcrF74Q', + authWalletAddress: '5uUQw7ZtTRYduT6MrsPQeGKAavRek2VzxAqgUxQE2szv', + }); + should.equal(txJson.instructionsData.length, 2); + should.deepEqual(txJson.instructionsData[1].params, { + stakingAddress: 'J8cECxcT6Q6H4fcQCvd4LbhmmSjsHL63kpJtrUcrF74Q', + oldAuthorizeAddress: '6xgesG4vajCYfAQpknodrarD49ZCnXGvYA4H1DLuGV7Y', + newAuthorizeAddress: '4p1VdN6BngTAbWR7Q5JPpbB6dc4k4y8wn1knmmWEjc9i', + custodianAddress: 'DHCVjKy7kN6D6vM69nHcEeEeS685qtonFbiFNBW5bGiq', + }); + }); + + it('should validate raw transaction', async () => { + const txBuilder = factory.getStakingRawMsgAuthorizeBuilder(); + txBuilder.validateRawTransaction(testData.STAKING_AUTHORIZE_RAW_MSG_TXN); + should(() => txBuilder.validateRawTransaction(testData.STAKING_AUTHORIZE_UNSIGNED_TX)).throwError( + 'Invalid staking instruction data: 0100000064c9ead9aa6b65445acf4fa526080bcf53baaad5e0bdb99578c9fc233f9c1df500000000' + ); + should.throws(() => txBuilder.validateRawTransaction(testData.ATA_INIT_SIGNED_DIFF_OWNER_TX)); + should.throws(() => txBuilder.validateRawTransaction(testData.TRANSFER_SIGNED_TX_WITH_MEMO_AND_DURABLE_NONCE)); + should.throws(() => txBuilder.validateRawTransaction(testData.TRANSFER_UNSIGNED_TX_WITHOUT_MEMO)); + }); + + it('should fail wrong transaction message data', async () => { + const txBuilder = factory.getStakingRawMsgAuthorizeBuilder(); + should.throws(() => txBuilder.transactionMessage('wrong data')); + const txBuilder2 = factory.from(testData.TRANSFER_UNSIGNED_TX_WITH_MEMO); + const msg = ((await txBuilder2.build()) as Transaction).signablePayload.toString('base64'); + should.throws(() => txBuilder.transactionMessage(msg)); + }); + + it('should fail from transaction data', async () => { + const txBuilder = factory.getStakingRawMsgAuthorizeBuilder(); + should.throws(() => txBuilder.from(testData.ATA_INIT_SIGNED_DIFF_OWNER_TX)); + should.throws(() => txBuilder.from(testData.TRANSFER_SIGNED_TX_WITH_MEMO_AND_DURABLE_NONCE)); + should.throws(() => txBuilder.from(testData.TRANSFER_UNSIGNED_TX_WITHOUT_MEMO)); + }); +}); diff --git a/modules/sdk-core/src/account-lib/baseCoin/enum.ts b/modules/sdk-core/src/account-lib/baseCoin/enum.ts index a8e0d99b6b..49d9e1d7cc 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/enum.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/enum.ts @@ -56,6 +56,8 @@ export enum TransactionType { StakingPledge, // Staking Authorize (e.g. SOL) StakingAuthorize, + // Staking Authorize from raw message (e.g. SOL) + StakingAuthorizeRaw, } /**