solana-web3: add TransferWithSeed implementation (#14570)

* fix: add handling for TransferWithSeed system instruction

* chore: add failing Assign/AllocateWithSeed test

* fix: broken Allocate/AssignWithSeed methods
This commit is contained in:
Tyera Eulberg 2021-01-14 09:59:31 -07:00 committed by GitHub
parent b37dbed479
commit 1eb7681a85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 291 additions and 11 deletions

17
web3.js/module.d.ts vendored
View File

@ -891,6 +891,15 @@ declare module '@solana/web3.js' {
lamports: number;
};
export type TransferWithSeedParams = {
fromPubkey: PublicKey;
basePubkey: PublicKey;
toPubkey: PublicKey;
lamports: number;
seed: string;
programId: PublicKey;
};
export type CreateNonceAccountParams = {
fromPubkey: PublicKey;
noncePubkey: PublicKey;
@ -943,7 +952,9 @@ declare module '@solana/web3.js' {
static assign(
params: AssignParams | AssignWithSeedParams,
): TransactionInstruction;
static transfer(params: TransferParams): TransactionInstruction;
static transfer(
params: TransferParams | TransferWithSeedParams,
): TransactionInstruction;
static createNonceAccount(
params: CreateNonceAccountParams | CreateNonceAccountWithSeedParams,
): Transaction;
@ -960,6 +971,7 @@ declare module '@solana/web3.js' {
| 'Assign'
| 'AssignWithSeed'
| 'Transfer'
| 'TransferWithSeed'
| 'AdvanceNonceAccount'
| 'WithdrawNonceAccount'
| 'InitializeNonceAccount'
@ -988,6 +1000,9 @@ declare module '@solana/web3.js' {
instruction: TransactionInstruction,
): AssignWithSeedParams;
static decodeTransfer(instruction: TransactionInstruction): TransferParams;
static decodeTransferWithSeed(
instruction: TransactionInstruction,
): TransferWithSeedParams;
static decodeNonceInitialize(
instruction: TransactionInstruction,
): InitializeNonceParams;

View File

@ -899,6 +899,15 @@ declare module '@solana/web3.js' {
lamports: number,
|};
declare export type TransferWithSeedParams = {|
fromPubkey: PublicKey,
basePubkey: PublicKey,
toPubkey: PublicKey,
lamports: number,
seed: string,
programId: PublicKey,
|};
declare export type CreateNonceAccountParams = {|
fromPubkey: PublicKey,
noncePubkey: PublicKey,
@ -951,7 +960,9 @@ declare module '@solana/web3.js' {
static assign(
params: AssignParams | AssignWithSeedParams,
): TransactionInstruction;
static transfer(params: TransferParams): TransactionInstruction;
static transfer(
params: TransferParams | TransferWithSeedParams,
): TransactionInstruction;
static createNonceAccount(
params: CreateNonceAccountParams | CreateNonceAccountWithSeedParams,
): Transaction;
@ -968,6 +979,7 @@ declare module '@solana/web3.js' {
| 'Assign'
| 'AssignWithSeed'
| 'Transfer'
| 'TransferWithSeed'
| 'AdvanceNonceAccount'
| 'WithdrawNonceAccount'
| 'InitializeNonceAccount'
@ -996,6 +1008,9 @@ declare module '@solana/web3.js' {
instruction: TransactionInstruction,
): AssignWithSeedParams;
static decodeTransfer(instruction: TransactionInstruction): TransferParams;
static decodeTransferWithSeed(
instruction: TransactionInstruction,
): TransferWithSeedParams;
static decodeNonceInitialize(
instruction: TransactionInstruction,
): InitializeNonceParams;

View File

@ -198,6 +198,23 @@ export type AssignWithSeedParams = {|
programId: PublicKey,
|};
/**
* Transfer with seed system transaction params
* @typedef {Object} TransferWithSeedParams
* @property {PublicKey} accountPubkey
* @property {PublicKey} basePubkey
* @property {string} seed
* @property {PublicKey} programId
*/
export type TransferWithSeedParams = {|
fromPubkey: PublicKey,
basePubkey: PublicKey,
toPubkey: PublicKey,
lamports: number,
seed: string,
programId: PublicKey,
|};
/**
* System Instruction class
*/
@ -269,6 +286,30 @@ export class SystemInstruction {
};
}
/**
* Decode a transfer with seed system instruction and retrieve the instruction params.
*/
static decodeTransferWithSeed(
instruction: TransactionInstruction,
): TransferWithSeedParams {
this.checkProgramId(instruction.programId);
this.checkKeyLength(instruction.keys, 3);
const {lamports, seed, programId} = decodeData(
SYSTEM_INSTRUCTION_LAYOUTS.TransferWithSeed,
instruction.data,
);
return {
fromPubkey: instruction.keys[0].pubkey,
basePubkey: instruction.keys[1].pubkey,
toPubkey: instruction.keys[2].pubkey,
lamports,
seed,
programId: new PublicKey(programId),
};
}
/**
* Decode an allocate system instruction and retrieve the instruction params.
*/
@ -576,6 +617,15 @@ export const SYSTEM_INSTRUCTION_LAYOUTS = Object.freeze({
Layout.publicKey('programId'),
]),
},
TransferWithSeed: {
index: 11,
layout: BufferLayout.struct([
BufferLayout.u32('instruction'),
BufferLayout.ns64('lamports'),
Layout.rustString('seed'),
Layout.publicKey('programId'),
]),
},
});
/**
@ -613,15 +663,34 @@ export class SystemProgram {
/**
* Generate a transaction instruction that transfers lamports from one account to another
*/
static transfer(params: TransferParams): TransactionInstruction {
const type = SYSTEM_INSTRUCTION_LAYOUTS.Transfer;
const data = encodeData(type, {lamports: params.lamports});
return new TransactionInstruction({
keys: [
static transfer(
params: TransferParams | TransferWithSeedParams,
): TransactionInstruction {
let data;
let keys;
if (params.basePubkey) {
const type = SYSTEM_INSTRUCTION_LAYOUTS.TransferWithSeed;
data = encodeData(type, {
lamports: params.lamports,
seed: params.seed,
programId: params.programId.toBuffer(),
});
keys = [
{pubkey: params.fromPubkey, isSigner: false, isWritable: true},
{pubkey: params.basePubkey, isSigner: true, isWritable: false},
{pubkey: params.toPubkey, isSigner: false, isWritable: true},
];
} else {
const type = SYSTEM_INSTRUCTION_LAYOUTS.Transfer;
data = encodeData(type, {lamports: params.lamports});
keys = [
{pubkey: params.fromPubkey, isSigner: true, isWritable: true},
{pubkey: params.toPubkey, isSigner: false, isWritable: true},
],
];
}
return new TransactionInstruction({
keys,
programId: this.programId,
data,
});
@ -634,6 +703,7 @@ export class SystemProgram {
params: AssignParams | AssignWithSeedParams,
): TransactionInstruction {
let data;
let keys;
if (params.basePubkey) {
const type = SYSTEM_INSTRUCTION_LAYOUTS.AssignWithSeed;
data = encodeData(type, {
@ -641,13 +711,18 @@ export class SystemProgram {
seed: params.seed,
programId: params.programId.toBuffer(),
});
keys = [
{pubkey: params.accountPubkey, isSigner: false, isWritable: true},
{pubkey: params.basePubkey, isSigner: true, isWritable: false},
];
} else {
const type = SYSTEM_INSTRUCTION_LAYOUTS.Assign;
data = encodeData(type, {programId: params.programId.toBuffer()});
keys = [{pubkey: params.accountPubkey, isSigner: true, isWritable: true}];
}
return new TransactionInstruction({
keys: [{pubkey: params.accountPubkey, isSigner: true, isWritable: true}],
keys,
programId: this.programId,
data,
});
@ -822,6 +897,7 @@ export class SystemProgram {
params: AllocateParams | AllocateWithSeedParams,
): TransactionInstruction {
let data;
let keys;
if (params.basePubkey) {
const type = SYSTEM_INSTRUCTION_LAYOUTS.AllocateWithSeed;
data = encodeData(type, {
@ -830,15 +906,20 @@ export class SystemProgram {
space: params.space,
programId: params.programId.toBuffer(),
});
keys = [
{pubkey: params.accountPubkey, isSigner: false, isWritable: true},
{pubkey: params.basePubkey, isSigner: true, isWritable: false},
];
} else {
const type = SYSTEM_INSTRUCTION_LAYOUTS.Allocate;
data = encodeData(type, {
space: params.space,
});
keys = [{pubkey: params.accountPubkey, isSigner: true, isWritable: true}];
}
return new TransactionInstruction({
keys: [{pubkey: params.accountPubkey, isSigner: true, isWritable: true}],
keys,
programId: this.programId,
data,
});

View File

@ -3,6 +3,7 @@
import {
Account,
Connection,
PublicKey,
StakeProgram,
SystemInstruction,
SystemProgram,
@ -52,6 +53,23 @@ test('transfer', () => {
expect(params).toEqual(SystemInstruction.decodeTransfer(systemInstruction));
});
test('transferWithSeed', () => {
const params = {
fromPubkey: new Account().publicKey,
basePubkey: new Account().publicKey,
toPubkey: new Account().publicKey,
lamports: 123,
seed: '你好',
programId: new Account().publicKey,
};
const transaction = new Transaction().add(SystemProgram.transfer(params));
expect(transaction.instructions).toHaveLength(1);
const [systemInstruction] = transaction.instructions;
expect(params).toEqual(
SystemInstruction.decodeTransferWithSeed(systemInstruction),
);
});
test('allocate', () => {
const params = {
accountPubkey: new Account().publicKey,
@ -402,3 +420,154 @@ test('live Nonce actions', async () => {
);
expect(withdrawBalance).toEqual(minimumAmount);
});
test('live withSeed actions', async () => {
if (mockRpcEnabled) {
console.log('non-live test skipped');
return;
}
const connection = new Connection(url, 'singleGossip');
const baseAccount = await newAccountWithLamports(
connection,
2 * LAMPORTS_PER_SOL,
);
const basePubkey = baseAccount.publicKey;
const seed = 'hi there';
const programId = new Account().publicKey;
const createAccountWithSeedAddress = await PublicKey.createWithSeed(
basePubkey,
seed,
programId,
);
const space = 0;
const minimumAmount = await connection.getMinimumBalanceForRentExemption(
space,
);
const createAccountWithSeedParams = {
fromPubkey: basePubkey,
newAccountPubkey: createAccountWithSeedAddress,
basePubkey,
seed,
lamports: minimumAmount,
space,
programId,
};
const createAccountWithSeedTransaction = new Transaction().add(
SystemProgram.createAccountWithSeed(createAccountWithSeedParams),
);
await sendAndConfirmTransaction(
connection,
createAccountWithSeedTransaction,
[baseAccount],
{commitment: 'singleGossip', preflightCommitment: 'singleGossip'},
);
const createAccountWithSeedBalance = await connection.getBalance(
createAccountWithSeedAddress,
);
expect(createAccountWithSeedBalance).toEqual(minimumAmount);
// Transfer to a derived address
const programId2 = new Account().publicKey;
const transferWithSeedAddress = await PublicKey.createWithSeed(
basePubkey,
seed,
programId2,
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
SystemProgram.transfer({
fromPubkey: baseAccount.publicKey,
toPubkey: transferWithSeedAddress,
lamports: 3 * minimumAmount,
}),
),
[baseAccount],
{commitment: 'singleGossip', preflightCommitment: 'singleGossip'},
);
let transferWithSeedAddressBalance = await connection.getBalance(
transferWithSeedAddress,
);
expect(transferWithSeedAddressBalance).toEqual(3 * minimumAmount);
// Test TransferWithSeed
const programId3 = new Account();
const toPubkey = await PublicKey.createWithSeed(
basePubkey,
seed,
programId3.publicKey,
);
const transferWithSeedParams = {
fromPubkey: transferWithSeedAddress,
basePubkey,
toPubkey,
lamports: 2 * minimumAmount,
seed,
programId: programId2,
};
const transferWithSeedTransaction = new Transaction().add(
SystemProgram.transfer(transferWithSeedParams),
);
await sendAndConfirmTransaction(
connection,
transferWithSeedTransaction,
[baseAccount],
{commitment: 'singleGossip', preflightCommitment: 'singleGossip'},
);
const toBalance = await connection.getBalance(toPubkey);
expect(toBalance).toEqual(2 * minimumAmount);
transferWithSeedAddressBalance = await connection.getBalance(
createAccountWithSeedAddress,
);
expect(transferWithSeedAddressBalance).toEqual(minimumAmount);
// Test AllocateWithSeed
const allocateWithSeedParams = {
accountPubkey: toPubkey,
basePubkey,
seed,
space: 10,
programId: programId3.publicKey,
};
const allocateWithSeedTransaction = new Transaction().add(
SystemProgram.allocate(allocateWithSeedParams),
);
await sendAndConfirmTransaction(
connection,
allocateWithSeedTransaction,
[baseAccount],
{commitment: 'singleGossip', preflightCommitment: 'singleGossip'},
);
let account = await connection.getAccountInfo(toPubkey);
if (account === null) {
expect(account).not.toBeNull();
return;
}
expect(account.data).toHaveLength(10);
// Test AssignWithSeed
const assignWithSeedParams = {
accountPubkey: toPubkey,
basePubkey,
seed,
programId: programId3.publicKey,
};
const assignWithSeedTransaction = new Transaction().add(
SystemProgram.assign(assignWithSeedParams),
);
await sendAndConfirmTransaction(
connection,
assignWithSeedTransaction,
[baseAccount],
{commitment: 'singleGossip', preflightCommitment: 'singleGossip'},
);
account = await connection.getAccountInfo(toPubkey);
if (account === null) {
expect(account).not.toBeNull();
return;
}
expect(account.owner).toEqual(programId3.publicKey);
});