feat: add token methods (#11303)

This commit is contained in:
Justin Starry
2020-07-31 12:33:54 +08:00
committed by GitHub
parent a5b6fd3d9b
commit 5a69c66877
7 changed files with 509 additions and 1 deletions

View File

@ -0,0 +1,46 @@
// flow-typed signature: 069c76ac6f0b539483367fd95c435241
// flow-typed version: <<STUB>>/@solana/spl-token_v0.0.4/flow_v0.130.0
/**
* This is an autogenerated libdef stub for:
*
* '@solana/spl-token'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module '@solana/spl-token' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module '@solana/spl-token/lib/index.cjs' {
declare module.exports: any;
}
declare module '@solana/spl-token/lib/index.esm' {
declare module.exports: any;
}
declare module '@solana/spl-token/module.flow' {
declare module.exports: any;
}
// Filename aliases
declare module '@solana/spl-token/lib/index.cjs.js' {
declare module.exports: $Exports<'@solana/spl-token/lib/index.cjs'>;
}
declare module '@solana/spl-token/lib/index.esm.js' {
declare module.exports: $Exports<'@solana/spl-token/lib/index.esm'>;
}
declare module '@solana/spl-token/module.flow.js' {
declare module.exports: $Exports<'@solana/spl-token/module.flow'>;
}

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

@ -50,6 +50,14 @@ declare module '@solana/web3.js' {
skipPreflight?: boolean; skipPreflight?: boolean;
}; };
export type TokenAccountsFilter =
| {
mint: PublicKey;
}
| {
programId: PublicKey;
};
export type RpcResponseAndContext<T> = { export type RpcResponseAndContext<T> = {
context: Context; context: Context;
value: T; value: T;
@ -150,6 +158,23 @@ declare module '@solana/web3.js' {
root: number; root: number;
}; };
export type TokenAccountInfo = {
mint: PublicKey;
owner: PublicKey;
amount: number;
delegate: null | PublicKey;
delegatedAmount: number;
isInitialized: boolean;
isNative: boolean;
};
export type TokenAccount = {
executable: boolean;
owner: PublicKey;
lamports: number;
data: TokenAccountInfo;
};
export type AccountChangeCallback = ( export type AccountChangeCallback = (
accountInfo: AccountInfo, accountInfo: AccountInfo,
context: Context, context: Context,
@ -239,6 +264,21 @@ declare module '@solana/web3.js' {
getMinimumLedgerSlot(): Promise<number>; getMinimumLedgerSlot(): Promise<number>;
getFirstAvailableBlock(): Promise<number>; getFirstAvailableBlock(): Promise<number>;
getSupply(commitment?: Commitment): Promise<RpcResponseAndContext<Supply>>; getSupply(commitment?: Commitment): Promise<RpcResponseAndContext<Supply>>;
getTokenSupply(
tokenMintAddress: PublicKey,
commitment?: Commitment,
): Promise<RpcResponseAndContext<number>>;
getTokenAccountBalance(
tokenAddress: PublicKey,
commitment?: Commitment,
): Promise<RpcResponseAndContext<number>>;
getTokenAccountsByOwner(
ownerAddress: PublicKey,
filter: TokenAccountsFilter,
commitment?: Commitment,
): Promise<
RpcResponseAndContext<Array<{pubkey: PublicKey; account: TokenAccount}>>
>;
getLargestAccounts( getLargestAccounts(
config?: GetLargestAccountsConfig, config?: GetLargestAccountsConfig,
): Promise<RpcResponseAndContext<Array<AccountBalancePair>>>; ): Promise<RpcResponseAndContext<Array<AccountBalancePair>>>;

View File

@ -66,6 +66,14 @@ declare module '@solana/web3.js' {
skipPreflight: ?boolean, skipPreflight: ?boolean,
}; };
declare export type TokenAccountsFilter =
| {
mint: PublicKey,
}
| {
programId: PublicKey,
};
declare export type RpcResponseAndContext<T> = { declare export type RpcResponseAndContext<T> = {
context: Context, context: Context,
value: T, value: T,
@ -166,6 +174,23 @@ declare module '@solana/web3.js' {
root: number, root: number,
}; };
declare export type TokenAccountInfo = {
mint: PublicKey,
owner: PublicKey,
amount: number,
delegate: null | PublicKey,
delegatedAmount: number,
isInitialized: boolean,
isNative: boolean,
};
declare export type TokenAccount = {
executable: boolean,
owner: PublicKey,
lamports: number,
data: TokenAccountInfo,
};
declare type AccountChangeCallback = ( declare type AccountChangeCallback = (
accountInfo: AccountInfo, accountInfo: AccountInfo,
context: Context, context: Context,
@ -255,6 +280,21 @@ declare module '@solana/web3.js' {
getMinimumLedgerSlot(): Promise<number>; getMinimumLedgerSlot(): Promise<number>;
getFirstAvailableBlock(): Promise<number>; getFirstAvailableBlock(): Promise<number>;
getSupply(commitment: ?Commitment): Promise<RpcResponseAndContext<Supply>>; getSupply(commitment: ?Commitment): Promise<RpcResponseAndContext<Supply>>;
getTokenSupply(
tokenMintAddress: PublicKey,
commitment: ?Commitment,
): Promise<RpcResponseAndContext<number>>;
getTokenAccountBalance(
tokenAddress: PublicKey,
commitment: ?Commitment,
): Promise<RpcResponseAndContext<number>>;
getTokenAccountsByOwner(
ownerAddress: PublicKey,
filter: TokenAccountsFilter,
commitment: ?Commitment,
): Promise<
RpcResponseAndContext<Array<{pubkey: PublicKey, account: TokenAccount}>>,
>;
getLargestAccounts( getLargestAccounts(
config: ?GetLargestAccountsConfig, config: ?GetLargestAccountsConfig,
): Promise<RpcResponseAndContext<Array<AccountBalancePair>>>; ): Promise<RpcResponseAndContext<Array<AccountBalancePair>>>;

View File

@ -4742,6 +4742,44 @@
"@sinonjs/commons": "^1.7.0" "@sinonjs/commons": "^1.7.0"
} }
}, },
"@solana/spl-token": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.0.4.tgz",
"integrity": "sha512-zYoZ6iYMKxGYbouunEkWdf6vWRJyEPOkAjvlNVjww9oPKMkIeM9VzgGtjZ/kKMelao1QEohH4JN9qXO4+LwfRA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.10.5",
"@solana/web3.js": "^0.63.2",
"bn.js": "^5.0.0",
"buffer-layout": "^1.2.0",
"dotenv": "8.2.0",
"json-to-pretty-yaml": "^1.2.2",
"mkdirp-promise": "^5.0.1"
}
},
"@solana/web3.js": {
"version": "0.63.2",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.63.2.tgz",
"integrity": "sha512-4jd8U1U/eFTEemr+jCzQCDepKnkttV4dxWsjMloifb82x1d6KgCzP+Jd6D9kr8f1MFj2i/AnG++97tlHAGTOkA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.3.1",
"bn.js": "^5.0.0",
"bs58": "^4.0.1",
"buffer": "^5.4.3",
"buffer-layout": "^1.2.0",
"crypto-hash": "^1.2.2",
"esdoc-inject-style-plugin": "^1.0.0",
"jayson": "^3.0.1",
"mz": "^2.7.0",
"node-fetch": "^2.2.0",
"npm-run-all": "^4.1.5",
"rpc-websockets": "^5.0.8",
"superstruct": "^0.8.3",
"tweetnacl": "^1.0.0",
"ws": "^7.0.0"
}
},
"@szmarczak/http-timer": { "@szmarczak/http-timer": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz",
@ -8274,6 +8312,12 @@
"is-obj": "^1.0.0" "is-obj": "^1.0.0"
} }
}, },
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true
},
"duplexer2": { "duplexer2": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@ -14170,6 +14214,16 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
}, },
"json-to-pretty-yaml": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz",
"integrity": "sha1-9M0L0KXo/h3yWq9boRiwmf2ZLVs=",
"dev": true,
"requires": {
"remedial": "^1.0.7",
"remove-trailing-spaces": "^1.0.6"
}
},
"json5": { "json5": {
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
@ -15150,6 +15204,15 @@
"minimist": "0.0.8" "minimist": "0.0.8"
} }
}, },
"mkdirp-promise": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz",
"integrity": "sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE=",
"dev": true,
"requires": {
"mkdirp": "*"
}
},
"modify-values": { "modify-values": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz",
@ -20096,12 +20159,24 @@
} }
} }
}, },
"remedial": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz",
"integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==",
"dev": true
},
"remove-trailing-separator": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
"dev": true "dev": true
}, },
"remove-trailing-spaces": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.7.tgz",
"integrity": "sha512-wjM17CJ2kk0SgoGyJ7ZMzRRCuTq+V8YhMwpZ5XEWX0uaked2OUq6utvHXGNBQrfkUzUUABFMyxlKn+85hMv4dg==",
"dev": true
},
"repeat-element": { "repeat-element": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",

View File

@ -97,6 +97,7 @@
"@babel/preset-flow": "^7.0.0", "@babel/preset-flow": "^7.0.0",
"@commitlint/config-conventional": "^9.0.1", "@commitlint/config-conventional": "^9.0.1",
"@commitlint/travis-cli": "^9.0.1", "@commitlint/travis-cli": "^9.0.1",
"@solana/spl-token": "^0.0.4",
"@typescript-eslint/eslint-plugin": "^2.18.0", "@typescript-eslint/eslint-plugin": "^2.18.0",
"@typescript-eslint/parser": "^2.18.0", "@typescript-eslint/parser": "^2.18.0",
"acorn": "^7.0.0", "acorn": "^7.0.0",

View File

@ -24,6 +24,14 @@ export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
type RpcRequest = (methodName: string, args: Array<any>) => any; type RpcRequest = (methodName: string, args: Array<any>) => any;
type TokenAccountsFilter =
| {
mint: PublicKey,
}
| {
programId: PublicKey,
};
/** /**
* Extra contextual information for RPC responses * Extra contextual information for RPC responses
* *
@ -500,6 +508,77 @@ const GetSupplyRpcResult = jsonRpcResultAndContext(
}), }),
); );
/**
* Information describing a token account
*/
type TokenAccountInfo = {|
mint: PublicKey,
owner: PublicKey,
amount: number,
delegate: null | PublicKey,
delegatedAmount: number,
isInitialized: boolean,
isNative: boolean,
|};
/**
* Information describing an account with token account data
*
* @typedef {Object} TokenAccount
* @property {number} lamports Number of lamports assigned to the account
* @property {PublicKey} owner Identifier of the program that owns the account
* @property {TokenAccountInfo} data Token account data
* @property {boolean} executable `true` if this account's data contains a loaded program
*/
type TokenAccount = {
executable: boolean,
owner: PublicKey,
lamports: number,
data: TokenAccountInfo,
};
const TokenAccountResult = struct({
token: struct({
account: struct({
mint: 'string',
owner: 'string',
amount: 'number',
delegate: struct.union(['string', 'null']),
delegatedAmount: 'number',
isInitialized: 'boolean',
isNative: 'boolean',
}),
}),
});
/**
* Expected JSON RPC response for the "getTokenAccountBalance" message
*/
const GetTokenAccountBalance = jsonRpcResultAndContext('number');
/**
* Expected JSON RPC response for the "getTokenSupply" message
*/
const GetTokenSupplyRpcResult = jsonRpcResultAndContext('number');
/**
* Expected JSON RPC response for the "getTokenAccountsByOwner" message
*/
const GetTokenAccountsByOwner = jsonRpcResultAndContext(
struct.array([
struct({
pubkey: 'string',
account: struct({
executable: 'boolean',
owner: 'string',
lamports: 'number',
data: TokenAccountResult,
rentEpoch: 'number?',
}),
}),
]),
);
/** /**
* Pair of an account address and its balance * Pair of an account address and its balance
* *
@ -541,7 +620,7 @@ const AccountInfoResult = struct({
executable: 'boolean', executable: 'boolean',
owner: 'string', owner: 'string',
lamports: 'number', lamports: 'number',
data: 'string', data: 'any',
rentEpoch: 'number?', rentEpoch: 'number?',
}); });
@ -1185,6 +1264,109 @@ export class Connection {
return res.result; return res.result;
} }
/**
* Fetch the current supply of a token mint
*/
async getTokenSupply(
tokenMintAddress: PublicKey,
commitment: ?Commitment,
): Promise<RpcResponseAndContext<number>> {
const args = this._argsWithCommitment(
[tokenMintAddress.toBase58()],
commitment,
);
const unsafeRes = await this._rpcRequest('getTokenSupply', args);
const res = GetTokenSupplyRpcResult(unsafeRes);
if (res.error) {
throw new Error('failed to get token supply: ' + res.error.message);
}
assert(typeof res.result !== 'undefined');
return res.result;
}
/**
* Fetch the current balance of a token account
*/
async getTokenAccountBalance(
tokenAddress: PublicKey,
commitment: ?Commitment,
): Promise<RpcResponseAndContext<number>> {
const args = this._argsWithCommitment(
[tokenAddress.toBase58()],
commitment,
);
const unsafeRes = await this._rpcRequest('getTokenAccountBalance', args);
const res = GetTokenAccountBalance(unsafeRes);
if (res.error) {
throw new Error(
'failed to get token account balance: ' + res.error.message,
);
}
assert(typeof res.result !== 'undefined');
return res.result;
}
/**
* Fetch all the token accounts owned by the specified account
*
* @return {Promise<RpcResponseAndContext<Array<{pubkey: PublicKey, account: TokenAccount}>>>}
*/
async getTokenAccountsByOwner(
ownerAddress: PublicKey,
filter: TokenAccountsFilter,
commitment: ?Commitment,
): Promise<
RpcResponseAndContext<Array<{pubkey: PublicKey, account: TokenAccount}>>,
> {
let _args = [ownerAddress.toBase58()];
// Strip flow types to make flow happy
((filter: any) => {
if ('mint' in filter) {
_args.push({mint: filter.mint.toBase58()});
} else {
_args.push({programId: filter.programId.toBase58()});
}
})(filter);
const args = this._argsWithCommitment(_args, commitment);
const unsafeRes = await this._rpcRequest('getTokenAccountsByOwner', args);
const res = GetTokenAccountsByOwner(unsafeRes);
if (res.error) {
throw new Error(
'failed to get token accounts owned by account ' +
ownerAddress.toBase58() +
': ' +
res.error.message,
);
}
const {result} = res;
const {context, value} = result;
assert(typeof result !== 'undefined');
return {
context,
value: value.map(result => {
const data = result.account.data.token.account;
return {
pubkey: new PublicKey(result.pubkey),
account: {
executable: result.account.executable,
owner: new PublicKey(result.account.owner),
lamports: result.account.lamports,
data: {
...data,
mint: new PublicKey(data.mint),
owner: new PublicKey(data.owner),
delegate: data.delegate ? new PublicKey(data.delegate) : null,
},
},
};
}),
};
}
/** /**
* Fetch the 20 largest accounts with their current balances * Fetch the 20 largest accounts with their current balances
*/ */

View File

@ -1,5 +1,6 @@
// @flow // @flow
import bs58 from 'bs58'; import bs58 from 'bs58';
import {Token, u64} from '@solana/spl-token';
import { import {
Account, Account,
@ -1261,6 +1262,129 @@ test('get supply', async () => {
expect(supply.nonCirculatingAccounts.length).toBeGreaterThan(0); expect(supply.nonCirculatingAccounts.length).toBeGreaterThan(0);
}); });
const TOKEN_PROGRAM_ID = new PublicKey(
'TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o',
);
test('token methods', async () => {
if (mockRpcEnabled) {
console.log('non-live test skipped');
return;
}
const connection = new Connection(url);
const payerAccount = new Account();
await connection.confirmTransaction(
await connection.requestAirdrop(payerAccount.publicKey, 100000000000),
0,
);
const mintOwner = new Account();
const accountOwner = new Account();
const [token, tokenAccount] = await Token.createMint(
connection,
payerAccount,
mintOwner.publicKey,
accountOwner.publicKey,
new u64(10000),
2,
TOKEN_PROGRAM_ID,
false,
);
await Token.createMint(
connection,
payerAccount,
mintOwner.publicKey,
accountOwner.publicKey,
new u64(10000),
2,
TOKEN_PROGRAM_ID,
false,
);
const tokenAccountDest = await token.createAccount(accountOwner.publicKey);
await token.transfer(
tokenAccount,
tokenAccountDest,
accountOwner,
[],
new u64(1),
);
const supply = (await connection.getTokenSupply(token.publicKey, 'recent'))
.value;
expect(supply).toEqual(10000);
const newAccount = new Account();
await expect(
connection.getTokenSupply(newAccount.publicKey, 'recent'),
).rejects.toThrow();
const balance = (
await connection.getTokenAccountBalance(tokenAccount, 'recent')
).value;
expect(balance).toEqual(9999);
await expect(
connection.getTokenAccountBalance(newAccount.publicKey, 'recent'),
).rejects.toThrow();
const accountsWithMintFilter = (
await connection.getTokenAccountsByOwner(
accountOwner.publicKey,
{mint: token.publicKey},
'recent',
)
).value;
expect(accountsWithMintFilter.length).toEqual(2);
for (const {account} of accountsWithMintFilter) {
expect(account.data.mint.toBase58()).toEqual(token.publicKey.toBase58());
expect(account.data.owner.toBase58()).toEqual(
accountOwner.publicKey.toBase58(),
);
}
const accountsWithProgramFilter = (
await connection.getTokenAccountsByOwner(
accountOwner.publicKey,
{programId: TOKEN_PROGRAM_ID},
'recent',
)
).value;
expect(accountsWithProgramFilter.length).toEqual(3);
for (const {account} of accountsWithProgramFilter) {
expect(account.data.owner.toBase58()).toEqual(
accountOwner.publicKey.toBase58(),
);
}
const noAccounts = (
await connection.getTokenAccountsByOwner(
newAccount.publicKey,
{mint: token.publicKey},
'recent',
)
).value;
expect(noAccounts.length).toEqual(0);
await expect(
connection.getTokenAccountsByOwner(
accountOwner.publicKey,
{mint: newAccount.publicKey},
'recent',
),
).rejects.toThrow();
await expect(
connection.getTokenAccountsByOwner(
accountOwner.publicKey,
{programId: newAccount.publicKey},
'recent',
),
).rejects.toThrow();
});
test('get largest accounts', async () => { test('get largest accounts', async () => {
const connection = new Connection(url); const connection = new Connection(url);