Skip to content

Commit

Permalink
Merge pull request #3780 from BitGo/EA-1169-sdk-create-raw-message-au…
Browse files Browse the repository at this point in the history
…thorize-builder

feat(sdk-coin-sol): add raw msg authorize builder
  • Loading branch information
ewangbitgo authored Aug 9, 2023
2 parents 5541570 + 649b7df commit 20a6fe6
Show file tree
Hide file tree
Showing 12 changed files with 427 additions and 9 deletions.
3 changes: 3 additions & 0 deletions modules/sdk-coin-sol/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ export const ataInitInstructionIndexes = {
InitializeAssociatedTokenAccount: 0,
Memo: 1,
} as const;

export const nonceAdvanceInstruction = 'AdvanceNonceAccount';
export const validInstructionData = '0a00000001000000';
16 changes: 15 additions & 1 deletion modules/sdk-coin-sol/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-coin-sol/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
35 changes: 35 additions & 0 deletions modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<Nonce | StakingAuthorize> {
const instructionData: Array<Nonce | StakingAuthorize> = [];
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;

Expand Down
137 changes: 137 additions & 0 deletions modules/sdk-coin-sol/src/lib/stakingRawMsgAuthorizeBuilder.ts
Original file line number Diff line number Diff line change
@@ -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<CoinConfig>) {
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<Transaction> {
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<Transaction> {
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');
}
}
75 changes: 72 additions & 3 deletions modules/sdk-coin-sol/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Memo,
Nonce,
StakingActivate,
StakingAuthorizeParams,
StakingWithdraw,
TokenTransfer,
TransactionExplanation,
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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(),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
};
}
}
Loading

0 comments on commit 20a6fe6

Please sign in to comment.