feat: Add ERC20-like Token
This commit is contained in:
parent
ab2d6c9ede
commit
ad2fa3ceaf
|
@ -92,4 +92,8 @@ declare module '@solana/web3.js' {
|
||||||
sign(from: Account): void;
|
sign(from: Account): void;
|
||||||
serialize(): Buffer;
|
serialize(): Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === src/token-program.js ===
|
||||||
|
/* TODO */
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@ type AccountInfo = {
|
||||||
*
|
*
|
||||||
* @typedef {string} SignatureStatus
|
* @typedef {string} SignatureStatus
|
||||||
*/
|
*/
|
||||||
type SignatureStatus = 'Confirmed' | 'SignatureNotFound' | 'ProgramRuntimeError' | 'GenericFailure';
|
export type SignatureStatus = 'Confirmed' | 'SignatureNotFound' | 'ProgramRuntimeError' | 'GenericFailure';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A connection to a fullnode JSON RPC endpoint
|
* A connection to a fullnode JSON RPC endpoint
|
||||||
|
|
|
@ -5,3 +5,4 @@ export {Connection} from './connection';
|
||||||
export {PublicKey} from './publickey';
|
export {PublicKey} from './publickey';
|
||||||
export {SystemProgram} from './system-program';
|
export {SystemProgram} from './system-program';
|
||||||
export {Transaction} from './transaction';
|
export {Transaction} from './transaction';
|
||||||
|
export {Token} from './token-program';
|
||||||
|
|
|
@ -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 => {
|
export const uint64 = (property: string = 'uint64'): Object => {
|
||||||
return BufferLayout.blob(32, property);
|
return BufferLayout.blob(8, property);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ export class PublicKey {
|
||||||
/**
|
/**
|
||||||
* Create a new PublicKey object
|
* Create a new PublicKey object
|
||||||
*/
|
*/
|
||||||
constructor(number: string | Buffer | Array<number>) {
|
constructor(number: number | string | Buffer | Array<number>) {
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (typeof number === 'string') {
|
if (typeof number === 'string') {
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,13 +18,15 @@ type RpcResponse = {
|
||||||
|
|
||||||
export const mockRpc: Array<[string, RpcRequest, 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
|
// Suppress lint: 'JestMockFn' is not defined
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const mock: JestMockFn<any, any> = jest.fn(
|
const mock: JestMockFn<any, any> = jest.fn(
|
||||||
(fetchUrl, fetchOptions) => {
|
(fetchUrl, fetchOptions) => {
|
||||||
// Define DOITLIVE in the environment to test against the real full node
|
if (!mockRpcEnabled) {
|
||||||
// identified by `url` instead of using the mock
|
|
||||||
if (process.env.DOITLIVE) {
|
|
||||||
console.log(`Note: node-fetch mock is disabled, testing live against ${fetchUrl}`);
|
console.log(`Note: node-fetch mock is disabled, testing live against ${fetchUrl}`);
|
||||||
return fetch(fetchUrl, fetchOptions);
|
return fetch(fetchUrl, fetchOptions);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,7 @@ import {Account} from '../src/account';
|
||||||
import {Connection} from '../src/connection';
|
import {Connection} from '../src/connection';
|
||||||
import {SystemProgram} from '../src/system-program';
|
import {SystemProgram} from '../src/system-program';
|
||||||
import {mockRpc} from './__mocks__/node-fetch';
|
import {mockRpc} from './__mocks__/node-fetch';
|
||||||
|
import {url} from './url.js';
|
||||||
let url = 'http://localhost:8899';
|
|
||||||
//url = 'http://testnet.solana.com:8899';
|
|
||||||
//url = 'http://master.testnet.solana.com:8899';
|
|
||||||
|
|
||||||
const errorMessage = 'Invalid request';
|
const errorMessage = 'Invalid request';
|
||||||
const errorResponse = {
|
const errorResponse = {
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue