feat: Add ERC20-like Token

This commit is contained in:
Michael Vines 2018-10-06 11:23:18 -07:00
parent ab2d6c9ede
commit ad2fa3ceaf
10 changed files with 1015 additions and 12 deletions

View File

@ -92,4 +92,8 @@ declare module '@solana/web3.js' {
sign(from: Account): void;
serialize(): Buffer;
}
// === src/token-program.js ===
/* TODO */
}

View File

@ -148,7 +148,7 @@ type AccountInfo = {
*
* @typedef {string} SignatureStatus
*/
type SignatureStatus = 'Confirmed' | 'SignatureNotFound' | 'ProgramRuntimeError' | 'GenericFailure';
export type SignatureStatus = 'Confirmed' | 'SignatureNotFound' | 'ProgramRuntimeError' | 'GenericFailure';
/**
* A connection to a fullnode JSON RPC endpoint

View File

@ -5,3 +5,4 @@ export {Connection} from './connection';
export {PublicKey} from './publickey';
export {SystemProgram} from './system-program';
export {Transaction} from './transaction';
export {Token} from './token-program';

View File

@ -11,8 +11,8 @@ export const publicKey = (property: string = 'publicKey'): Object => {
};
/**
* Layout for a 256bit unsigned value
* Layout for a 64bit unsigned value
*/
export const uint256 = (property: string = 'uint256'): Object => {
return BufferLayout.blob(32, property);
export const uint64 = (property: string = 'uint64'): Object => {
return BufferLayout.blob(8, property);
};

View File

@ -12,7 +12,7 @@ export class PublicKey {
/**
* Create a new PublicKey object
*/
constructor(number: string | Buffer | Array<number>) {
constructor(number: number | string | Buffer | Array<number>) {
for (;;) {
if (typeof number === 'string') {

View File

@ -0,0 +1,476 @@
/**
* @flow
*/
import assert from 'assert';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
import * as Layout from './layout';
import {
Account,
PublicKey,
SystemProgram,
Transaction,
} from '.';
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
import type {Connection} from '.';
/**
* @private
*/
const RustStringLayout = (property: string = 'rustString') => {
const lengthLayout = BufferLayout.u8('length');
const rsl = BufferLayout.struct(
[
lengthLayout,
BufferLayout.seq(BufferLayout.u8(), 7, 'u8 into u64 padding'), // TODO: avoid padding
BufferLayout.blob(
BufferLayout.offset(lengthLayout, -8),
'chars',
),
],
property,
);
const _decode = rsl.decode.bind(rsl);
const _encode = rsl.encode.bind(rsl);
rsl.decode = (buffer, offset) => {
const data = _decode(buffer, offset);
return data.chars.toString('utf8');
};
rsl.encode = (str, buffer, offset) => {
const data = {
chars: Buffer.from(str, 'utf8'),
};
return _encode(data, buffer, offset);
};
return rsl;
};
/**
* Some amount of tokens
*/
export class TokenAmount extends BN {
/**
* Convert to Buffer representation
*/
toBuffer(): Buffer {
const a = super.toArray().reverse();
const b = Buffer.from(a);
if (b.length === 8) {
return b;
}
assert(b.length < 8, 'TokenAmount too large');
const zeroPad = Buffer.alloc(8);
b.copy(zeroPad);
return zeroPad;
}
/**
* Construct a TokenAmount from Buffer representation
*/
static fromBuffer(buffer: Buffer): TokenAmount {
assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`);
return new BN(
[...buffer].reverse().map(i => `00${i.toString(16)}`.slice(-2)).join(''),
16
);
}
}
/**
* Information about a token
*/
type TokenInfo = {
/**
* Total supply of tokens
*/
supply: TokenAmount,
/**
* Number of base 10 digits to the right of the decimal place
*/
decimals: number,
/**
* Descriptive name of this token
*/
name: string,
/**
* Symbol for this token
*/
symbol: string,
};
/**
* @private
*/
const TokenInfoLayout = BufferLayout.struct([
Layout.uint64('supply'),
BufferLayout.u8('decimals'),
new RustStringLayout('name'),
new RustStringLayout('symbol'),
]);
/**
* Information about a token account
*/
type TokenAccountInfo = {
/**
* The kind of token this account holds
*/
token: PublicKey,
/**
* Owner of this account
*/
owner: PublicKey,
/**
* Amount of tokens this account holds
*/
amount: TokenAmount,
/**
* The source account for the tokens.
*
* If `source` is null, the source is this account.
* If `source` is not null, the `amount` of tokens in this account represent
* an allowance of tokens that may be transferred from the source account
*/
source: null | PublicKey,
};
/**
* @private
*/
const TokenAccountInfoLayout = BufferLayout.struct([
Layout.publicKey('token'),
Layout.publicKey('owner'),
Layout.uint64('amount'),
BufferLayout.u8('sourceOption'),
Layout.publicKey('source'),
]);
type TokenAndPublicKey = [Token, PublicKey]; // This type exists to workaround an esdoc parse error
/**
* An ERC20-like Token
*/
export class Token {
/**
* @private
*/
connection: Connection;
/**
* The public key identifying this token
*/
token: PublicKey;
/**
* Create a Token object attached to the specific token
*
* @param connection The connection to use
* @param token Public key of the token
*/
constructor(connection: Connection, token: PublicKey) {
Object.assign(this, {connection, token});
}
/**
* Create a new Token
*
* @param connection The connection to use
* @param owner User account that will own the returned Token Account
* @param supply Total supply of the new token
* @param name Descriptive name of this token
* @param symbol Symbol for this token
* @param decimals Location of the decimal place
* @return Token object for the newly minted token, Public key of the Token Account holding the total supply of new tokens
*/
static async createNewToken(
connection: Connection,
owner: Account,
supply: TokenAmount,
name: string,
symbol: string,
decimals: number,
): Promise<TokenAndPublicKey> {
const tokenAccount = new Account();
const token = new Token(connection, tokenAccount.publicKey);
const initialAccountPublicKey = (await token._newAccount(owner, null)).publicKey;
let transaction;
const userdataLayout = BufferLayout.struct([
BufferLayout.u32('instruction'),
Layout.uint64('supply'),
BufferLayout.u8('decimals'),
new RustStringLayout('name'),
new RustStringLayout('symbol'),
]);
let userdata = Buffer.alloc(1024);
{
const encodeLength = userdataLayout.encode(
{
instruction: 0, // NewToken instruction
supply: supply.toBuffer(),
decimals,
name,
symbol,
},
userdata,
);
userdata = userdata.slice(0, encodeLength);
}
// Allocate memory for the tokenAccount account
transaction = SystemProgram.createAccount(
owner.publicKey,
tokenAccount.publicKey,
1,
1 + userdata.length,
Token.programId,
);
await sendAndConfirmTransaction(connection, owner, transaction);
transaction = new Transaction({
fee: 0,
keys: [tokenAccount.publicKey, initialAccountPublicKey],
programId: Token.programId,
userdata,
});
await sendAndConfirmTransaction(connection, tokenAccount, transaction);
return [token, initialAccountPublicKey];
}
/**
* @private
*/
async _newAccount(owner: Account, source: null | PublicKey): Promise<Account> {
const tokenAccount = new Account();
let transaction;
const userdataLayout = BufferLayout.struct([
BufferLayout.u32('instruction'),
]);
const userdata = Buffer.alloc(userdataLayout.span);
userdataLayout.encode(
{
instruction: 1, // NewTokenAccount instruction
},
userdata,
);
// Allocate memory for the token
transaction = SystemProgram.createAccount(
owner.publicKey,
tokenAccount.publicKey,
1,
1 + TokenAccountInfoLayout.span,
Token.programId,
);
await sendAndConfirmTransaction(this.connection, owner, transaction);
// Initialize the token account
const keys = [tokenAccount.publicKey, owner.publicKey, this.token];
if (source) {
keys.push(source);
}
transaction = new Transaction({
fee: 0,
keys,
programId: Token.programId,
userdata,
});
await sendAndConfirmTransaction(this.connection, tokenAccount, transaction);
return tokenAccount;
}
/**
* Create a new and empty token account.
*
* This account may then be used as a `transfer()` or `approve()` destination
*
* @param owner User account that will own the new token account
* @param source If not null, create a delegate account that when authorized
* may transfer tokens from this `source` account
* @return Public key of the new empty token account
*/
async newAccount(owner: Account, source: null | PublicKey = null): Promise<PublicKey> {
return (await this._newAccount(owner, source)).publicKey;
}
/**
* Retrieve token information
*/
async tokenInfo(): Promise<TokenInfo> {
const accountInfo = await this.connection.getAccountInfo(this.token);
if (!accountInfo.programId.equals(Token.programId)) {
throw new Error(`Invalid token programId: ${JSON.stringify(accountInfo.programId)}`);
}
const userdata = Buffer.from(accountInfo.userdata);
if (userdata.readUInt8(0) !== 1) {
throw new Error(`Invalid token userdata`);
}
const tokenInfo = TokenInfoLayout.decode(userdata, 1);
tokenInfo.supply = TokenAmount.fromBuffer(tokenInfo.supply);
return tokenInfo;
}
/**
* Retrieve account information
*
* @param account Public key of the token account
*/
async accountInfo(account: PublicKey): Promise<TokenAccountInfo> {
const accountInfo = await this.connection.getAccountInfo(account);
if (!accountInfo.programId.equals(Token.programId)) {
throw new Error(`Invalid token account programId`);
}
const userdata = Buffer.from(accountInfo.userdata);
if (userdata.readUInt8(0) !== 2) {
throw new Error(`Invalid token account userdata`);
}
const tokenAccountInfo = TokenAccountInfoLayout.decode(userdata, 1);
tokenAccountInfo.token = new PublicKey(tokenAccountInfo.token);
tokenAccountInfo.owner = new PublicKey(tokenAccountInfo.owner);
tokenAccountInfo.amount = TokenAmount.fromBuffer(tokenAccountInfo.amount);
tokenAccountInfo.source = tokenAccountInfo.sourceOption === 0 ? null : new PublicKey(tokenAccountInfo.source);
if (!tokenAccountInfo.token.equals(this.token)) {
throw new Error(
`Invalid token account token: ${JSON.stringify(tokenAccountInfo.token)} !== ${JSON.stringify(this.token)}`
);
}
return tokenAccountInfo;
}
/**
* Transfer tokens to another account
*
* @param owner Owner of the source token account
* @param source Source token account
* @param destination Destination token account
* @param amount Number of tokens to transfer
*/
async transfer(
owner: Account,
source: PublicKey,
destination: PublicKey,
amount: number | TokenAmount,
): Promise<void> {
const accountInfo = await this.accountInfo(source);
if (!owner.publicKey.equals(accountInfo.owner)) {
throw new Error('Account owner mismatch');
}
const userdataLayout = BufferLayout.struct([
BufferLayout.u32('instruction'),
Layout.uint64('amount'),
]);
const userdata = Buffer.alloc(userdataLayout.span);
userdataLayout.encode(
{
instruction: 2, // Transfer instruction
amount: (new TokenAmount(amount)).toBuffer(),
},
userdata,
);
const keys = [owner.publicKey, source, destination];
if (accountInfo.source) {
keys.push(accountInfo.source);
}
const transaction = new Transaction({
fee: 0,
keys,
programId: Token.programId,
userdata,
});
await sendAndConfirmTransaction(this.connection, owner, transaction);
}
/**
* Grant a third-party permission to transfer up the specified number of tokens from an account
*
* @param owner Owner of the source token account
* @param source Source token account
* @param delegate Token account authorized to perform a transfer tokens from the source account
* @param amount Maximum number of tokens the delegate may transfer
*/
async approve(
owner: Account,
source: PublicKey,
delegate: PublicKey,
amount: number | TokenAmount
): Promise<void> {
const userdataLayout = BufferLayout.struct([
BufferLayout.u32('instruction'),
Layout.uint64('amount'),
]);
const userdata = Buffer.alloc(userdataLayout.span);
userdataLayout.encode(
{
instruction: 3, // Approve instruction
amount: (new TokenAmount(amount)).toBuffer(),
},
userdata,
);
const transaction = new Transaction({
fee: 0,
keys: [owner.publicKey, source, delegate],
programId: Token.programId,
userdata,
});
await sendAndConfirmTransaction(this.connection, owner, transaction);
}
/**
* Remove approval for the transfer of any remaining tokens
*
* @param owner Owner of the source token account
* @param source Source token account
* @param delegate Token account to revoke authorization from
*/
revoke(
owner: Account,
source: PublicKey,
delegate: PublicKey
): Promise<void> {
return this.approve(owner, source, delegate, 0);
}
/**
* Program Identifier for the Token program
*/
static get programId(): PublicKey {
return new PublicKey('0x500000000000000000000000000000000000000000000000000000000000000');
}
}

View File

@ -18,13 +18,15 @@ type RpcResponse = {
export const mockRpc: Array<[string, RpcRequest, RpcResponse]> = [];
// Define DOITLIVE in the environment to test against the real full node
// identified by `url` instead of using the mock
export const mockRpcEnabled = !process.env.DOITLIVE;
// Suppress lint: 'JestMockFn' is not defined
// eslint-disable-next-line no-undef
const mock: JestMockFn<any, any> = jest.fn(
(fetchUrl, fetchOptions) => {
// Define DOITLIVE in the environment to test against the real full node
// identified by `url` instead of using the mock
if (process.env.DOITLIVE) {
if (!mockRpcEnabled) {
console.log(`Note: node-fetch mock is disabled, testing live against ${fetchUrl}`);
return fetch(fetchUrl, fetchOptions);
}

View File

@ -4,10 +4,7 @@ import {Account} from '../src/account';
import {Connection} from '../src/connection';
import {SystemProgram} from '../src/system-program';
import {mockRpc} from './__mocks__/node-fetch';
let url = 'http://localhost:8899';
//url = 'http://testnet.solana.com:8899';
//url = 'http://master.testnet.solana.com:8899';
import {url} from './url.js';
const errorMessage = 'Invalid request';
const errorResponse = {

View File

@ -0,0 +1,514 @@
// @flow
import {Account} from '../src/account';
import {Connection} from '../src/connection';
import {Token, TokenAmount} from '../src/token-program';
import {PublicKey} from '../src/publickey';
import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch';
import {url} from './url.js';
import type {SignatureStatus} from '../src/connection';
if (!mockRpcEnabled) {
// The default of 5 seconds is too slow for live testing sometimes
jest.setTimeout(10000);
}
function mockGetLastId() {
mockRpc.push([
url,
{
method: 'getLastId',
params: [],
},
{
error: null,
result: '2BjEqiiT43J6XskiHdz7aoocjPeWkCPiKD72SiFQsrA2',
}
]);
}
function mockGetSignatureStatus(result: SignatureStatus = 'Confirmed') {
mockRpc.push([
url,
{
method: 'getSignatureStatus',
},
{
error: null,
result,
},
]);
}
function mockSendTransaction() {
mockRpc.push([
url,
{
method: 'sendTransaction',
},
{
error: null,
result: '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}
]);
}
async function newAccountWithTokens(connection: Connection, amount: number = 10): Promise<Account> {
const account = new Account();
{
mockRpc.push([
url,
{
method: 'requestAirdrop',
params: [account.publicKey.toBase58(), amount],
},
{
error: null,
result: '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}
]);
}
await connection.requestAirdrop(account.publicKey, amount);
return account;
}
// A token created by the first test and used by all subsequent tests
let testToken: Token;
// Initial owner of the token supply
let initialOwner;
let initialOwnerTokenAccount: PublicKey;
test('create new token', async () => {
const connection = new Connection(url);
initialOwner = await newAccountWithTokens(connection);
{
// mock SystemProgram.createAccount transaction for Token.createNewToken()
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
// mock Token.newAccount() transaction
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus('SignatureNotFound');
mockGetSignatureStatus();
// mock SystemProgram.createAccount transaction for Token.createNewToken()
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
// mock Token.createNewToken() transaction
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus('SignatureNotFound');
mockGetSignatureStatus();
}
[testToken, initialOwnerTokenAccount] = await Token.createNewToken(
connection,
initialOwner,
new TokenAmount(10000),
'Test token',
'TEST',
2
);
{
// mock Token.tokenInfo()'s getAccountInfo
mockRpc.push([
url,
{
method: 'getAccountInfo',
params: [testToken.token.toBase58()],
},
{
error: null,
result: {
program_id: [...Token.programId.toBuffer()],
tokens: 1,
userdata: [
1,
16, 39, 0, 0, 0, 0, 0, 0,
2,
10, 0, 0, 0, 0, 0, 0, 0, 84, 101, 115, 116, 32, 116, 111, 107, 101, 110,
4, 0, 0, 0, 0, 0, 0, 0, 84, 69, 83, 84
],
}
}
]);
}
const tokenInfo = await testToken.tokenInfo();
expect(tokenInfo.supply.toNumber()).toBe(10000);
expect(tokenInfo.decimals).toBe(2);
expect(tokenInfo.name).toBe('Test token');
expect(tokenInfo.symbol).toBe('TEST');
{
// mock Token.accountInfo()'s getAccountInfo
mockRpc.push([
url,
{
method: 'getAccountInfo',
params: [initialOwnerTokenAccount.toBase58()],
},
{
error: null,
result: {
program_id: [...Token.programId.toBuffer()],
tokens: 1,
userdata: [
2,
...testToken.token.toBuffer(),
...initialOwner.publicKey.toBuffer(),
16, 39, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
}
}
]);
}
const accountInfo = await testToken.accountInfo(initialOwnerTokenAccount);
expect(accountInfo.token.equals(testToken.token)).toBe(true);
expect(accountInfo.owner.equals(initialOwner.publicKey)).toBe(true);
expect(accountInfo.amount.toNumber()).toBe(10000);
expect(accountInfo.source).toBe(null);
});
test('create new token account', async () => {
const connection = new Connection(url);
const destOwner = await newAccountWithTokens(connection);
{
// mock SystemProgram.createAccount transaction for Token.newAccount()
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
// mock Token.newAccount() transaction
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
}
const dest = await testToken.newAccount(destOwner);
{
// mock Token.accountInfo()'s getAccountInfo
mockRpc.push([
url,
{
method: 'getAccountInfo',
params: [dest.toBase58()],
},
{
error: null,
result: {
program_id: [...Token.programId.toBuffer()],
tokens: 1,
userdata: [
2,
...testToken.token.toBuffer(),
...destOwner.publicKey.toBuffer(),
0, 0, 0, 0, 0, 0, 0, 0,
0,
],
}
}
]);
}
const accountInfo = await testToken.accountInfo(dest);
expect(accountInfo.token.equals(testToken.token)).toBe(true);
expect(accountInfo.owner.equals(destOwner.publicKey)).toBe(true);
expect(accountInfo.amount.toNumber()).toBe(0);
expect(accountInfo.source).toBe(null);
});
test('transfer', async () => {
const connection = new Connection(url);
const destOwner = await newAccountWithTokens(connection);
{
// mock SystemProgram.createAccount transaction for Token.newAccount()
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
// mock Token.newAccount() transaction
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
}
const dest = await testToken.newAccount(destOwner);
{
// mock Token.transfer()'s getAccountInfo
mockRpc.push([
url,
{
method: 'getAccountInfo',
params: [initialOwnerTokenAccount.toBase58()],
},
{
error: null,
result: {
program_id: [...Token.programId.toBuffer()],
tokens: 1,
userdata: [
2,
...testToken.token.toBuffer(),
...initialOwner.publicKey.toBuffer(),
123, 0, 0, 0, 0, 0, 0, 0,
0,
],
}
}
]);
// mock Token.transfer() transaction
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
}
await testToken.transfer(
initialOwner,
initialOwnerTokenAccount,
dest,
123
);
{
// mock Token.accountInfo()'s getAccountInfo
mockRpc.push([
url,
{
method: 'getAccountInfo',
params: [dest.toBase58()],
},
{
error: null,
result: {
program_id: [...Token.programId.toBuffer()],
tokens: 1,
userdata: [
2,
...testToken.token.toBuffer(),
...dest.toBuffer(),
123, 0, 0, 0, 0, 0, 0, 0,
0,
],
}
}
]);
}
const destAccountInfo = await testToken.accountInfo(dest);
expect(destAccountInfo.amount.toNumber()).toBe(123);
});
test('approve/revoke', async () => {
const connection = new Connection(url);
const delegateOwner = await newAccountWithTokens(connection);
{
// mock SystemProgram.createAccount transaction for Token.newAccount()
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
// mock Token.newAccount() transaction
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
}
const delegate = await testToken.newAccount(delegateOwner, initialOwnerTokenAccount);
{
// mock Token.approve() transaction
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
}
await testToken.approve(
initialOwner,
initialOwnerTokenAccount,
delegate,
456
);
{
// mock Token.accountInfo()'s getAccountInfo
mockRpc.push([
url,
{
method: 'getAccountInfo',
params: [delegate.toBase58()],
},
{
error: null,
result: {
program_id: [...Token.programId.toBuffer()],
tokens: 1,
userdata: [
2,
...testToken.token.toBuffer(),
...delegate.toBuffer(),
200, 1, 0, 0, 0, 0, 0, 0,
1,
...initialOwnerTokenAccount.toBuffer(),
],
}
}
]);
}
let delegateAccountInfo = await testToken.accountInfo(delegate);
expect(delegateAccountInfo.amount.toNumber()).toBe(456);
if (delegateAccountInfo.source === null) {
throw new Error('source should not be null');
} else {
expect(delegateAccountInfo.source.equals(initialOwnerTokenAccount)).toBe(true);
}
{
// mock Token.revoke() transaction
mockGetLastId();
mockSendTransaction();
mockGetSignatureStatus();
}
await testToken.revoke(
initialOwner,
initialOwnerTokenAccount,
delegate,
);
{
// mock Token.accountInfo()'s getAccountInfo
mockRpc.push([
url,
{
method: 'getAccountInfo',
params: [delegate.toBase58()],
},
{
error: null,
result: {
program_id: [...Token.programId.toBuffer()],
tokens: 1,
userdata: [
2,
...testToken.token.toBuffer(),
...delegate.toBuffer(),
0, 0, 0, 0, 0, 0, 0, 0,
1,
...initialOwnerTokenAccount.toBuffer(),
],
}
}
]);
}
delegateAccountInfo = await testToken.accountInfo(delegate);
expect(delegateAccountInfo.amount.toNumber()).toBe(0);
if (delegateAccountInfo.source === null) {
throw new Error('source should not be null');
} else {
expect(delegateAccountInfo.source.equals(initialOwnerTokenAccount)).toBe(true);
}
});
test('invalid approve', async () => {
if (mockRpcEnabled) {
console.log('non-live test skipped');
return;
}
const connection = new Connection(url);
const owner = await newAccountWithTokens(connection);
const account1 = await testToken.newAccount(owner);
const account2 = await testToken.newAccount(owner);
// account2 is not a delegate account
expect(
testToken.approve(
owner,
account1,
account2,
123
)
).rejects.toThrow();
});
test('fail on approve overspend', async () => {
if (mockRpcEnabled) {
console.log('non-live test skipped');
return;
}
const connection = new Connection(url);
const owner = await newAccountWithTokens(connection);
const account1 = await testToken.newAccount(owner);
const account1Delegate = await testToken.newAccount(owner, account1);
const account2 = await testToken.newAccount(owner);
await testToken.transfer(
initialOwner,
initialOwnerTokenAccount,
account1,
10,
);
await testToken.approve(
owner,
account1,
account1Delegate,
2
);
await testToken.transfer(
owner,
account1Delegate,
account2,
1,
);
await testToken.transfer(
owner,
account1Delegate,
account2,
1,
);
expect(
testToken.transfer(
owner,
account1Delegate,
account2,
1,
)
).rejects.toThrow();
});

9
web3.js/test/url.js Normal file
View File

@ -0,0 +1,9 @@
// @flow
/**
* The connection url to use when running unit tests against a live network
*/
export const url = 'http://localhost:8899';
//export const url = 'http://testnet.solana.com:8899';
//export const url = 'http://master.testnet.solana.com:8899';