fix: pay transaction fees from system accounts

This commit is contained in:
Michael Vines
2019-05-08 09:33:04 -07:00
parent 7bde7e3767
commit 48f0bcc2bf
10 changed files with 133 additions and 166 deletions

View File

@ -249,9 +249,13 @@ declare module '@solana/web3.js' {
// === src/loader.js === // === src/loader.js ===
declare export class Loader { declare export class Loader {
constructor(connection: Connection, programId: PublicKey): Loader; static load(
load(program: Account, offset: number, bytes: Array<number>): Promise<void>; connection: Connection,
finalize(program: Account): Promise<void>; payer: Account,
program: Account,
programId: PublicKey,
data: Array<number>,
): Promise<PublicKey>;
} }
// === src/bpf-loader.js === // === src/bpf-loader.js ===
@ -259,7 +263,7 @@ declare module '@solana/web3.js' {
static programId: PublicKey; static programId: PublicKey;
static load( static load(
connection: Connection, connection: Connection,
owner: Account, payer: Account,
elfBytes: Array<number>, elfBytes: Array<number>,
): Promise<PublicKey>; ): Promise<PublicKey>;
} }
@ -269,7 +273,7 @@ declare module '@solana/web3.js' {
static programId: PublicKey; static programId: PublicKey;
static load( static load(
connection: Connection, connection: Connection,
owner: Account, payer: Account,
programName: string, programName: string,
): Promise<PublicKey>; ): Promise<PublicKey>;
} }

View File

@ -3,8 +3,6 @@
import {Account} from './account'; import {Account} from './account';
import {PublicKey} from './publickey'; import {PublicKey} from './publickey';
import {Loader} from './loader'; import {Loader} from './loader';
import {SystemProgram} from './system-program';
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
import type {Connection} from './connection'; import type {Connection} from './connection';
/** /**
@ -15,9 +13,7 @@ export class BpfLoader {
* Public key that identifies the BpfLoader * Public key that identifies the BpfLoader
*/ */
static get programId(): PublicKey { static get programId(): PublicKey {
return new PublicKey( return new PublicKey('BPFLoader1111111111111111111111111111111111');
'BPFLoader1111111111111111111111111111111111',
);
} }
/** /**
@ -27,26 +23,12 @@ export class BpfLoader {
* @param owner User account to load the program into * @param owner User account to load the program into
* @param elfBytes The entire ELF containing the BPF program * @param elfBytes The entire ELF containing the BPF program
*/ */
static async load( static load(
connection: Connection, connection: Connection,
owner: Account, payer: Account,
elf: Array<number>, elf: Array<number>,
): Promise<PublicKey> { ): Promise<PublicKey> {
const programAccount = new Account(); const program = new Account();
return Loader.load(connection, payer, program, BpfLoader.programId, elf);
const transaction = SystemProgram.createAccount(
owner.publicKey,
programAccount.publicKey,
1 + Math.ceil(elf.length / Loader.chunkSize) + 1,
elf.length,
BpfLoader.programId,
);
await sendAndConfirmTransaction(connection, transaction, owner);
const loader = new Loader(connection, BpfLoader.programId);
await loader.load(programAccount, elf);
await loader.finalize(programAccount);
return programAccount.publicKey;
} }
} }

View File

@ -143,9 +143,7 @@ export class BudgetProgram {
* Public key that identifies the Budget program * Public key that identifies the Budget program
*/ */
static get programId(): PublicKey { static get programId(): PublicKey {
return new PublicKey( return new PublicKey('Budget1111111111111111111111111111111111111');
'Budget1111111111111111111111111111111111111',
);
} }
/** /**

View File

@ -626,7 +626,6 @@ export class Connection {
subscriptionId, subscriptionId,
programId, programId,
} = this._programAccountChangeSubscriptions[id]; } = this._programAccountChangeSubscriptions[id];
console.log('program-id: ' + programId);
if (subscriptionId === null) { if (subscriptionId === null) {
try { try {
this._programAccountChangeSubscriptions[ this._programAccountChangeSubscriptions[

View File

@ -9,43 +9,46 @@ import {Transaction} from './transaction';
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction'; import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
import {sleep} from './util/sleep'; import {sleep} from './util/sleep';
import type {Connection} from './connection'; import type {Connection} from './connection';
import {SystemProgram} from './system-program';
/** /**
* Program loader interface * Program loader interface
*/ */
export class Loader { export class Loader {
/**
* @private
*/
connection: Connection;
/**
* @private
*/
programId: PublicKey;
/** /**
* Amount of program data placed in each load Transaction * Amount of program data placed in each load Transaction
*/ */
static get chunkSize(): number { static get chunkSize(): number {
return 256; return 229; // Keep program chunks under PACKET_DATA_SIZE
} }
/** /**
* @param connection The connection to use * Loads a generic program
* @param programId Public key that identifies the loader
*/
constructor(connection: Connection, programId: PublicKey) {
Object.assign(this, {connection, programId});
}
/**
* Load program data
* *
* @param program Account to load the program info * @param connection The connection to use
* @param data Program data * @param payer System account that pays to load the program
* @param program Account to load the program into
* @param programId Public key that identifies the loader
* @param data Program octets
*/ */
async load(program: Account, data: Array<number>) { static async load(
connection: Connection,
payer: Account,
program: Account,
programId: PublicKey,
data: Array<number>,
): Promise<PublicKey> {
{
const transaction = SystemProgram.createAccount(
payer.publicKey,
program.publicKey,
1,
data.length,
programId,
);
await sendAndConfirmTransaction(connection, transaction, payer);
}
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
BufferLayout.u32('instruction'), BufferLayout.u32('instruction'),
BufferLayout.u32('offset'), BufferLayout.u32('offset'),
@ -76,11 +79,11 @@ export class Loader {
const transaction = new Transaction().add({ const transaction = new Transaction().add({
keys: [{pubkey: program.publicKey, isSigner: true}], keys: [{pubkey: program.publicKey, isSigner: true}],
programId: this.programId, programId,
data, data,
}); });
transactions.push( transactions.push(
sendAndConfirmTransaction(this.connection, transaction, program), sendAndConfirmTransaction(connection, transaction, payer, program),
); );
// Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors // Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors
@ -100,14 +103,9 @@ export class Loader {
array = array.slice(chunkSize); array = array.slice(chunkSize);
} }
await Promise.all(transactions); await Promise.all(transactions);
}
/** // Finalize the account loaded with program data for execution
* Finalize an account loaded with program data for execution {
*
* @param program `load()`ed Account
*/
async finalize(program: Account) {
const dataLayout = BufferLayout.struct([BufferLayout.u32('instruction')]); const dataLayout = BufferLayout.struct([BufferLayout.u32('instruction')]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
@ -120,9 +118,11 @@ export class Loader {
const transaction = new Transaction().add({ const transaction = new Transaction().add({
keys: [{pubkey: program.publicKey, isSigner: true}], keys: [{pubkey: program.publicKey, isSigner: true}],
programId: this.programId, programId,
data, data,
}); });
await sendAndConfirmTransaction(this.connection, transaction, program); await sendAndConfirmTransaction(connection, transaction, payer, program);
}
return program.publicKey;
} }
} }

View File

@ -3,8 +3,6 @@
import {Account} from './account'; import {Account} from './account';
import {PublicKey} from './publickey'; import {PublicKey} from './publickey';
import {Loader} from './loader'; import {Loader} from './loader';
import {SystemProgram} from './system-program';
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
import type {Connection} from './connection'; import type {Connection} from './connection';
/** /**
@ -15,41 +13,29 @@ export class NativeLoader {
* Public key that identifies the NativeLoader * Public key that identifies the NativeLoader
*/ */
static get programId(): PublicKey { static get programId(): PublicKey {
return new PublicKey( return new PublicKey('NativeLoader1111111111111111111111111111111');
'NativeLoader1111111111111111111111111111111',
);
} }
/** /**
* Loads a native program * Loads a native program
* *
* @param connection The connection to use * @param connection The connection to use
* @param owner User account to load the program with * @param payer System account that pays to load the program
* @param programName Name of the native program * @param programName Name of the native program
*/ */
static async load( static load(
connection: Connection, connection: Connection,
owner: Account, payer: Account,
programName: string, programName: string,
): Promise<PublicKey> { ): Promise<PublicKey> {
const bytes = [...Buffer.from(programName)]; const bytes = [...Buffer.from(programName)];
const program = new Account();
const programAccount = new Account(); return Loader.load(
connection,
// Allocate memory for the program account payer,
const transaction = SystemProgram.createAccount( program,
owner.publicKey,
programAccount.publicKey,
1 + 1 + 1,
bytes.length + 1,
NativeLoader.programId, NativeLoader.programId,
bytes,
); );
await sendAndConfirmTransaction(connection, transaction, owner);
const loader = new Loader(connection, NativeLoader.programId);
await loader.load(programAccount, bytes);
await loader.finalize(programAccount);
return programAccount.publicKey;
} }
} }

View File

@ -243,7 +243,12 @@ export class Token {
programId, programId,
data, data,
}); });
await sendAndConfirmTransaction(connection, transaction, tokenAccount); await sendAndConfirmTransaction(
connection,
transaction,
owner,
tokenAccount,
);
return [token, initialAccountPublicKey]; return [token, initialAccountPublicKey];
} }
@ -299,7 +304,12 @@ export class Token {
programId: this.programId, programId: this.programId,
data, data,
}); });
await sendAndConfirmTransaction(this.connection, transaction, tokenAccount); await sendAndConfirmTransaction(
this.connection,
transaction,
owner,
tokenAccount,
);
return tokenAccount.publicKey; return tokenAccount.publicKey;
} }

View File

@ -92,7 +92,7 @@ export class Transaction {
signatures: Array<SignaturePubkeyPair> = []; signatures: Array<SignaturePubkeyPair> = [];
/** /**
* The first (primary) Transaction signature * The first (payer) Transaction signature
*/ */
get signature(): Buffer | null { get signature(): Buffer | null {
if (this.signatures.length > 0) { if (this.signatures.length > 0) {
@ -172,6 +172,14 @@ export class Transaction {
}); });
}); });
if (numRequiredSignatures > this.signatures.length) {
throw new Error(
`Insufficent signatures: expected ${numRequiredSignatures} but got ${
this.signatures.length
}`,
);
}
let keyCount = []; let keyCount = [];
shortvec.encodeLength(keyCount, keys.length); shortvec.encodeLength(keyCount, keys.length);
@ -252,7 +260,7 @@ export class Transaction {
]); ]);
const transaction = { const transaction = {
numRequiredSignatures: Buffer.from([numRequiredSignatures]), numRequiredSignatures: Buffer.from([this.signatures.length]),
keyCount: Buffer.from(keyCount), keyCount: Buffer.from(keyCount),
keys: keys.map(key => new PublicKey(key).toBuffer()), keys: keys.map(key => new PublicKey(key).toBuffer()),
recentBlockhash: Buffer.from(bs58.decode(recentBlockhash)), recentBlockhash: Buffer.from(bs58.decode(recentBlockhash)),

View File

@ -1,12 +1,5 @@
// @flow // @flow
import { import {Account, Connection, BpfLoader, Loader, SystemProgram} from '../src';
Account,
Connection,
BpfLoader,
Loader,
SystemProgram,
sendAndConfirmTransaction,
} from '../src';
import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from '../src/timing'; import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from '../src/timing';
import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch'; import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch';
import {mockGetRecentBlockhash} from './mockrpc/get-recent-blockhash'; import {mockGetRecentBlockhash} from './mockrpc/get-recent-blockhash';
@ -436,7 +429,11 @@ test('transaction', async () => {
result: 2, result: 2,
}, },
]); ]);
expect(await connection.getBalance(accountFrom.publicKey)).toBe(2);
// accountFrom may have less than 2 due to transaction fees
const balance = await connection.getBalance(accountFrom.publicKey);
expect(balance).toBeGreaterThan(0);
expect(balance).toBeLessThanOrEqual(2);
mockRpc.push([ mockRpc.push([
url, url,
@ -494,7 +491,12 @@ test('multi-instruction transaction', async () => {
Ok: null, Ok: null,
}); });
expect(await connection.getBalance(accountFrom.publicKey)).toBe(12); // accountFrom may have less than 12 due to transaction fees
expect(await connection.getBalance(accountFrom.publicKey)).toBeGreaterThan(0);
expect(
await connection.getBalance(accountFrom.publicKey),
).toBeLessThanOrEqual(12);
expect(await connection.getBalance(accountTo.publicKey)).toBe(21); expect(await connection.getBalance(accountTo.publicKey)).toBe(21);
}); });
@ -516,28 +518,21 @@ test('account change notification', async () => {
); );
await connection.requestAirdrop(owner.publicKey, 42); await connection.requestAirdrop(owner.publicKey, 42);
const transaction = SystemProgram.createAccount( await Loader.load(connection, owner, programAccount, BpfLoader.programId, [
owner.publicKey, 1,
programAccount.publicKey, 2,
42,
3, 3,
BpfLoader.programId, ]);
);
await sendAndConfirmTransaction(connection, transaction, owner);
const loader = new Loader(connection, BpfLoader.programId);
await loader.load(programAccount, [1, 2, 3]);
// Wait for mockCallback to receive a call // Wait for mockCallback to receive a call
let i = 0; let i = 0;
for (;;) { for (;;) {
if (mockCallback.mock.calls.length === 1) { if (mockCallback.mock.calls.length > 0) {
break; break;
} }
if (++i === 5) { if (++i === 30) {
console.log(JSON.stringify(mockCallback.mock.calls)); throw new Error('Account change notification not observed');
throw new Error('mockCallback should be called twice');
} }
// Sleep for a 1/4 of a slot, notifications only occur after a block is // Sleep for a 1/4 of a slot, notifications only occur after a block is
// processed // processed
@ -546,10 +541,8 @@ test('account change notification', async () => {
await connection.removeAccountChangeListener(subscriptionId); await connection.removeAccountChangeListener(subscriptionId);
expect(mockCallback.mock.calls[0][0].lamports).toBe(42); expect(mockCallback.mock.calls[0][0].lamports).toBe(1);
expect(mockCallback.mock.calls[0][0].owner).toEqual(BpfLoader.programId); expect(mockCallback.mock.calls[0][0].owner).toEqual(BpfLoader.programId);
expect(mockCallback.mock.calls[0][0].executable).toBe(false);
expect(mockCallback.mock.calls[0][0].data).toEqual(Buffer.from([1, 2, 3]));
}); });
test('program account change notification', async () => { test('program account change notification', async () => {
@ -562,36 +555,35 @@ test('program account change notification', async () => {
const owner = new Account(); const owner = new Account();
const programAccount = new Account(); const programAccount = new Account();
const mockCallback = jest.fn(); // const mockCallback = jest.fn();
let notified = false;
const subscriptionId = connection.onProgramAccountChange( const subscriptionId = connection.onProgramAccountChange(
BpfLoader.programId, BpfLoader.programId,
mockCallback, keyedAccountInfo => {
if (keyedAccountInfo.accountId !== programAccount.publicKey.toString()) {
//console.log('Ignoring another account', keyedAccountInfo);
return;
}
expect(keyedAccountInfo.accountInfo.lamports).toBe(1);
expect(keyedAccountInfo.accountInfo.owner).toEqual(BpfLoader.programId);
notified = true;
},
); );
await connection.requestAirdrop(owner.publicKey, 42); await connection.requestAirdrop(owner.publicKey, 42);
const transaction = SystemProgram.createAccount( await Loader.load(connection, owner, programAccount, BpfLoader.programId, [
owner.publicKey, 1,
programAccount.publicKey, 2,
42,
3, 3,
BpfLoader.programId, ]);
);
await sendAndConfirmTransaction(connection, transaction, owner);
const loader = new Loader(connection, BpfLoader.programId);
await loader.load(programAccount, [1, 2, 3]);
// Wait for mockCallback to receive a call // Wait for mockCallback to receive a call
let i = 0; let i = 0;
for (;;) { while (!notified) {
if (mockCallback.mock.calls.length === 1) { //for (;;) {
break; if (++i === 30) {
} throw new Error('Program change notification not observed');
if (++i === 20) {
console.log(JSON.stringify(mockCallback.mock.calls));
throw new Error('mockCallback should be called twice');
} }
// Sleep for a 1/4 of a slot, notifications only occur after a block is // Sleep for a 1/4 of a slot, notifications only occur after a block is
// processed // processed
@ -599,16 +591,4 @@ test('program account change notification', async () => {
} }
await connection.removeProgramAccountChangeListener(subscriptionId); await connection.removeProgramAccountChangeListener(subscriptionId);
expect(mockCallback.mock.calls[0][0].accountId).toEqual(
programAccount.publicKey.toString(),
);
expect(mockCallback.mock.calls[0][0].accountInfo.lamports).toBe(42);
expect(mockCallback.mock.calls[0][0].accountInfo.owner).toEqual(
BpfLoader.programId,
);
expect(mockCallback.mock.calls[0][0].accountInfo.executable).toBe(false);
expect(mockCallback.mock.calls[0][0].accountInfo.data).toEqual(
Buffer.from([1, 2, 3]),
);
}); });

View File

@ -6,7 +6,7 @@ import {url} from './url';
export async function newAccountWithLamports( export async function newAccountWithLamports(
connection: Connection, connection: Connection,
lamports: number = 10, lamports: number = 1000000,
): Promise<Account> { ): Promise<Account> {
const account = new Account(); const account = new Account();