Skip to content
This repository was archived by the owner on Aug 21, 2020. It is now read-only.

Commit 62b89ae

Browse files
authored
Rework token program (#108)
* nudge Add integration test * add test and convert to SimpleSerde * rewrite account handling * cleanup * nudge * Optimize serialization * C Representation structures * add test coverage * No output when running program test * Respect rent * update package-lock * Point to git, update npm scripts
1 parent 7413d7e commit 62b89ae

File tree

18 files changed

+4399
-447
lines changed

18 files changed

+4399
-447
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"type": "git",
77
"url": "https://github.com/solana-labs/example-token"
88
},
9+
"testnetDefaultChannel": "beta",
910
"scripts": {
1011
"start": "babel-node src/cli/main.js",
1112
"lint": "npm run pretty && eslint .",
@@ -15,7 +16,9 @@
1516
"lint:watch": "watch 'npm run lint:fix' . --wait=1",
1617
"bpf-sdk:update": "node_modules/@solana/web3.js/bin/bpf-sdk-install.sh",
1718
"build:program": "./src/program/do.sh build",
18-
"clean:program": "./src/program/do.sh clean",
19+
"clean:program": "./src/program/do.sh clean && cargo clean --manifest-path ./src/program-test/Cargo.toml",
20+
"test:program": "./src/program/do.sh test",
21+
"bench:program": "npm run build:program && cargo test --manifest-path ./src/program-test/Cargo.toml -- --nocapture",
1922
"localnet:update": "solana-localnet update",
2023
"localnet:up": "set -x; solana-localnet down; set -e; solana-localnet up",
2124
"localnet:down": "solana-localnet down",

src/cli/main.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ async function main() {
2828
await approveRevoke();
2929
console.log('Run test: invalidApprove');
3030
await invalidApprove();
31-
console.log('Run test: loadTokenProgram');
31+
console.log('Run test: failOnApproveOverspend');
3232
await failOnApproveOverspend();
33-
console.log('Run test: failOnApproveOverspends');
33+
console.log('Run test: setOwner');
3434
await setOwner();
3535
console.log('Success\n');
3636
}

src/cli/token-test.js

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -60,33 +60,33 @@ export async function loadTokenProgram(): Promise<void> {
6060
);
6161
const connection = await getConnection();
6262
const [, feeCalculator] = await connection.getRecentBlockhash();
63-
const fees =
63+
const balanceNeeded =
6464
feeCalculator.lamportsPerSignature *
65-
(BpfLoader.getMinNumSignatures(data.length) + NUM_RETRIES);
66-
const from = await newAccountWithLamports(connection, fees);
65+
(BpfLoader.getMinNumSignatures(data.length) + NUM_RETRIES) +
66+
(await connection.getMinimumBalanceForRentExemption(data.length));
6767

68-
console.log('Loading Token program, may take a bit...');
68+
const from = await newAccountWithLamports(connection, balanceNeeded);
69+
console.log('Loading Token program...');
6970
programId = await BpfLoader.load(connection, from, data);
7071
}
7172

7273
export async function createNewToken(): Promise<void> {
7374
const connection = await getConnection();
74-
initialOwner = await newAccountWithLamports(connection, 1024);
75+
const balanceNeeded =
76+
(await Token.getMinBalanceRentForExemptToken(connection)) +
77+
(await Token.getMinBalanceRentForExemptTokenAccount(connection));
78+
initialOwner = await newAccountWithLamports(connection, balanceNeeded);
7579
[testToken, initialOwnerTokenAccount] = await Token.createNewToken(
7680
connection,
7781
initialOwner,
7882
new TokenAmount(10000),
79-
'Test token',
80-
'TEST',
8183
2,
8284
programId,
8385
);
8486

8587
const tokenInfo = await testToken.tokenInfo();
8688
assert(tokenInfo.supply.toNumber() == 10000);
8789
assert(tokenInfo.decimals == 2);
88-
assert(tokenInfo.name == 'Test token');
89-
assert(tokenInfo.symbol == 'TEST');
9090

9191
const accountInfo = await testToken.accountInfo(initialOwnerTokenAccount);
9292
assert(accountInfo.token.equals(testToken.token));
@@ -98,7 +98,10 @@ export async function createNewToken(): Promise<void> {
9898

9999
export async function createNewTokenAccount(): Promise<void> {
100100
const connection = await getConnection();
101-
const destOwner = await newAccountWithLamports(connection);
101+
const balanceNeeded = await Token.getMinBalanceRentForExemptTokenAccount(
102+
connection,
103+
);
104+
const destOwner = await newAccountWithLamports(connection, balanceNeeded);
102105
const dest = await testToken.newAccount(destOwner);
103106
const accountInfo = await testToken.accountInfo(dest);
104107
assert(accountInfo.token.equals(testToken.token));
@@ -109,7 +112,10 @@ export async function createNewTokenAccount(): Promise<void> {
109112

110113
export async function transfer(): Promise<void> {
111114
const connection = await getConnection();
112-
const destOwner = await newAccountWithLamports(connection);
115+
const balanceNeeded = await Token.getMinBalanceRentForExemptTokenAccount(
116+
connection,
117+
);
118+
const destOwner = await newAccountWithLamports(connection, balanceNeeded);
113119
const dest = await testToken.newAccount(destOwner);
114120

115121
await testToken.transfer(initialOwner, initialOwnerTokenAccount, dest, 123);
@@ -126,7 +132,10 @@ export async function approveRevoke(): Promise<void> {
126132
}
127133

128134
const connection = await getConnection();
129-
const delegateOwner = await newAccountWithLamports(connection);
135+
const balanceNeeded = await Token.getMinBalanceRentForExemptTokenAccount(
136+
connection,
137+
);
138+
const delegateOwner = await newAccountWithLamports(connection, balanceNeeded);
130139
const delegate = await testToken.newAccount(
131140
delegateOwner,
132141
initialOwnerTokenAccount,
@@ -161,7 +170,9 @@ export async function approveRevoke(): Promise<void> {
161170

162171
export async function invalidApprove(): Promise<void> {
163172
const connection = await getConnection();
164-
const owner = await newAccountWithLamports(connection);
173+
const balanceNeeded =
174+
(await Token.getMinBalanceRentForExemptTokenAccount(connection)) * 3;
175+
const owner = await newAccountWithLamports(connection, balanceNeeded);
165176
const account1 = await testToken.newAccount(owner);
166177
const account1Delegate = await testToken.newAccount(owner, account1);
167178
const account2 = await testToken.newAccount(owner);
@@ -174,7 +185,9 @@ export async function invalidApprove(): Promise<void> {
174185

175186
export async function failOnApproveOverspend(): Promise<void> {
176187
const connection = await getConnection();
177-
const owner = await newAccountWithLamports(connection);
188+
const balanceNeeded =
189+
(await Token.getMinBalanceRentForExemptTokenAccount(connection)) * 3;
190+
const owner = await newAccountWithLamports(connection, balanceNeeded);
178191
const account1 = await testToken.newAccount(owner);
179192
const account1Delegate = await testToken.newAccount(owner, account1);
180193
const account2 = await testToken.newAccount(owner);
@@ -209,8 +222,11 @@ export async function failOnApproveOverspend(): Promise<void> {
209222

210223
export async function setOwner(): Promise<void> {
211224
const connection = await getConnection();
212-
const owner = await newAccountWithLamports(connection);
213-
const newOwner = await newAccountWithLamports(connection);
225+
const balanceNeeded = await Token.getMinBalanceRentForExemptTokenAccount(
226+
connection,
227+
);
228+
const owner = await newAccountWithLamports(connection, balanceNeeded);
229+
const newOwner = await newAccountWithLamports(connection, balanceNeeded);
214230
const account = await testToken.newAccount(owner);
215231

216232
await testToken.setOwner(owner, account, newOwner.publicKey);

src/client/token.js

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -65,26 +65,15 @@ type TokenInfo = {|
6565
* Number of base 10 digits to the right of the decimal place
6666
*/
6767
decimals: number,
68-
69-
/**
70-
* Descriptive name of this token
71-
*/
72-
name: string,
73-
74-
/**
75-
* Symbol for this token
76-
*/
77-
symbol: string,
7868
|};
7969

8070
/**
8171
* @private
8272
*/
8373
const TokenInfoLayout = BufferLayout.struct([
74+
BufferLayout.u8('state'),
8475
Layout.uint64('supply'),
8576
BufferLayout.u8('decimals'),
86-
Layout.rustString('name'),
87-
Layout.rustString('symbol'),
8877
]);
8978

9079
/**
@@ -126,10 +115,11 @@ type TokenAccountInfo = {|
126115
* @private
127116
*/
128117
const TokenAccountInfoLayout = BufferLayout.struct([
118+
BufferLayout.u8('state'),
129119
Layout.publicKey('token'),
130120
Layout.publicKey('owner'),
131121
Layout.uint64('amount'),
132-
BufferLayout.u8('sourceOption'),
122+
BufferLayout.nu64('sourceOption'),
133123
Layout.publicKey('source'),
134124
Layout.uint64('originalAmount'),
135125
]);
@@ -166,14 +156,38 @@ export class Token {
166156
Object.assign(this, {connection, token, programId});
167157
}
168158

159+
/**
160+
* Get the minimum balance for the token to be rent exempt
161+
*
162+
* @return Number of lamports required
163+
*/
164+
static async getMinBalanceRentForExemptToken(
165+
connection: Connection,
166+
): Promise<number> {
167+
return await connection.getMinimumBalanceForRentExemption(
168+
TokenInfoLayout.span,
169+
);
170+
}
171+
172+
/**
173+
* Get the minimum balance for the token account to be rent exempt
174+
*
175+
* @return Number of lamports required
176+
*/
177+
static async getMinBalanceRentForExemptTokenAccount(
178+
connection: Connection,
179+
): Promise<number> {
180+
return await connection.getMinimumBalanceForRentExemption(
181+
TokenAccountInfoLayout.span,
182+
);
183+
}
184+
169185
/**
170186
* Create a new Token
171187
*
172188
* @param connection The connection to use
173189
* @param owner User account that will own the returned Token Account
174190
* @param supply Total supply of the new token
175-
* @param name Descriptive name of this token
176-
* @param symbol Symbol for this token
177191
* @param decimals Location of the decimal place
178192
* @param programId Optional token programId, uses the system programId by default
179193
* @return Token object for the newly minted token, Public key of the Token Account holding the total supply of new tokens
@@ -182,8 +196,6 @@ export class Token {
182196
connection: Connection,
183197
owner: Account,
184198
supply: TokenAmount,
185-
name: string,
186-
symbol: string,
187199
decimals: number,
188200
programId: PublicKey,
189201
): Promise<TokenAndPublicKey> {
@@ -194,11 +206,9 @@ export class Token {
194206
let transaction;
195207

196208
const dataLayout = BufferLayout.struct([
197-
BufferLayout.u32('instruction'),
209+
BufferLayout.u8('instruction'),
198210
Layout.uint64('supply'),
199-
BufferLayout.u8('decimals'),
200-
Layout.rustString('name'),
201-
Layout.rustString('symbol'),
211+
BufferLayout.nu64('decimals'),
202212
]);
203213

204214
let data = Buffer.alloc(1024);
@@ -208,19 +218,21 @@ export class Token {
208218
instruction: 0, // NewToken instruction
209219
supply: supply.toBuffer(),
210220
decimals,
211-
name,
212-
symbol,
213221
},
214222
data,
215223
);
216224
data = data.slice(0, encodeLength);
217225
}
218226

227+
const balanceNeeded = await Token.getMinBalanceRentForExemptToken(
228+
connection,
229+
);
230+
219231
// Allocate memory for the tokenAccount account
220232
transaction = SystemProgram.createAccount(
221233
owner.publicKey,
222234
tokenAccount.publicKey,
223-
1,
235+
balanceNeeded,
224236
1 + data.length,
225237
programId,
226238
);
@@ -268,7 +280,7 @@ export class Token {
268280
const tokenAccount = new Account();
269281
let transaction;
270282

271-
const dataLayout = BufferLayout.struct([BufferLayout.u32('instruction')]);
283+
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
272284

273285
const data = Buffer.alloc(dataLayout.span);
274286
dataLayout.encode(
@@ -278,12 +290,16 @@ export class Token {
278290
data,
279291
);
280292

293+
const balanceNeeded = await Token.getMinBalanceRentForExemptTokenAccount(
294+
this.connection,
295+
);
296+
281297
// Allocate memory for the token
282298
transaction = SystemProgram.createAccount(
283299
owner.publicKey,
284300
tokenAccount.publicKey,
285-
1,
286-
1 + TokenAccountInfoLayout.span,
301+
balanceNeeded,
302+
TokenAccountInfoLayout.span,
287303
this.programId,
288304
);
289305
await sendAndConfirmTransaction(
@@ -332,10 +348,10 @@ export class Token {
332348

333349
const data = Buffer.from(accountInfo.data);
334350

335-
if (data.readUInt8(0) !== 1) {
336-
throw new Error(`Invalid token data`);
351+
const tokenInfo = TokenInfoLayout.decode(data);
352+
if (tokenInfo.state !== 1) {
353+
throw new Error(`Invalid token account data`);
337354
}
338-
const tokenInfo = TokenInfoLayout.decode(data, 1);
339355
tokenInfo.supply = TokenAmount.fromBuffer(tokenInfo.supply);
340356
return tokenInfo;
341357
}
@@ -352,11 +368,11 @@ export class Token {
352368
}
353369

354370
const data = Buffer.from(accountInfo.data);
355-
if (data.readUInt8(0) !== 2) {
371+
const tokenAccountInfo = TokenAccountInfoLayout.decode(data);
372+
373+
if (tokenAccountInfo.state !== 2) {
356374
throw new Error(`Invalid token account data`);
357375
}
358-
const tokenAccountInfo = TokenAccountInfoLayout.decode(data, 1);
359-
360376
tokenAccountInfo.token = new PublicKey(tokenAccountInfo.token);
361377
tokenAccountInfo.owner = new PublicKey(tokenAccountInfo.owner);
362378
tokenAccountInfo.amount = TokenAmount.fromBuffer(tokenAccountInfo.amount);
@@ -490,7 +506,7 @@ export class Token {
490506
}
491507

492508
const dataLayout = BufferLayout.struct([
493-
BufferLayout.u32('instruction'),
509+
BufferLayout.u8('instruction'),
494510
Layout.uint64('amount'),
495511
]);
496512

@@ -537,7 +553,7 @@ export class Token {
537553
amount: number | TokenAmount,
538554
): TransactionInstruction {
539555
const dataLayout = BufferLayout.struct([
540-
BufferLayout.u32('instruction'),
556+
BufferLayout.u8('instruction'),
541557
Layout.uint64('amount'),
542558
]);
543559

@@ -588,7 +604,7 @@ export class Token {
588604
account: PublicKey,
589605
newOwner: PublicKey,
590606
): TransactionInstruction {
591-
const dataLayout = BufferLayout.struct([BufferLayout.u32('instruction')]);
607+
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
592608

593609
const data = Buffer.alloc(dataLayout.span);
594610
dataLayout.encode(

src/client/util/new-account-with-lamports.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export async function newAccountWithLamports(
2020
if (--retries <= 0) {
2121
break;
2222
}
23-
console.log('Airdrop retry ' + retries);
23+
console.log('Airdrop retry ' + retries);
2424
}
2525
throw new Error(`Airdrop of ${lamports} failed`);
2626
}

src/program-test/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target/

0 commit comments

Comments
 (0)