[token-js] : Support for UiAmountToAmount and AmountToUiAmount instructions (#3345)

* suport for amountToUiAmount instruction

* support for uiamount to amount instruction

* upgrade @solana/web3.js to 1.47.4

* move amount.test from e2e-2022 to e2e

* fix and sort imports

Co-authored-by: Jordan Sexton <jordan@jordansexton.com>
This commit is contained in:
Athar Mohammad 2022-08-24 12:24:16 +05:30 committed by GitHub
parent e03a013184
commit 2fe0ce60f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 434 additions and 28 deletions

View File

@ -15,7 +15,7 @@
},
"devDependencies": {
"@solana/spl-memo": "^0.2.1",
"@solana/web3.js": "^1.20.0",
"@solana/web3.js": "^1.47.4",
"@types/chai": "^4.3.3",
"@types/chai-as-promised": "^7.1.4",
"@types/mocha": "^9.1.0",
@ -44,7 +44,7 @@
"node": ">=16"
},
"peerDependencies": {
"@solana/web3.js": "^1.20.0"
"@solana/web3.js": "^1.47.4"
}
},
"node_modules/@ampproject/remapping": {

View File

@ -49,7 +49,7 @@
"deploy:docs": "npm run docs && gh-pages --dist token/js --dotfiles"
},
"peerDependencies": {
"@solana/web3.js": "^1.20.0"
"@solana/web3.js": "^1.47.4"
},
"dependencies": {
"@solana/buffer-layout": "^4.0.0",
@ -58,7 +58,7 @@
},
"devDependencies": {
"@solana/spl-memo": "^0.2.1",
"@solana/web3.js": "^1.20.0",
"@solana/web3.js": "^1.47.4",
"@types/chai-as-promised": "^7.1.4",
"@types/chai": "^4.3.3",
"@types/mocha": "^9.1.0",

View File

@ -0,0 +1,30 @@
import type { Connection, PublicKey, Signer, TransactionError } from '@solana/web3.js';
import { Transaction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from '../constants.js';
import { createAmountToUiAmountInstruction } from '../instructions/amountToUiAmount.js';
/**
* Amount as a string using mint-prescribed decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Mint for the account
* @param amount Amount of tokens to be converted to Ui Amount
* @param programId SPL Token program account
*
* @return Ui Amount generated
*/
export async function amountToUiAmount(
connection: Connection,
payer: Signer,
mint: PublicKey,
amount: number | bigint,
programId = TOKEN_PROGRAM_ID
): Promise<string | TransactionError | null> {
const transaction = new Transaction().add(createAmountToUiAmountInstruction(mint, amount, programId));
const { returnData, err } = (await connection.simulateTransaction(transaction, [payer], false)).value;
if (returnData?.data) {
return Buffer.from(returnData.data[0], returnData.data[1]).toString('utf-8');
}
return err;
}

View File

@ -1,22 +1,23 @@
export * from './createMint.js';
export * from './createNativeMint.js';
export * from './createAccount.js';
export * from './createWrappedNativeAccount.js';
export * from './createMultisig.js';
export * from './transfer.js';
export * from './amountToUiAmount.js';
export * from './approve.js';
export * from './approveChecked.js';
export * from './burn.js';
export * from './burnChecked.js';
export * from './closeAccount.js';
export * from './createAccount.js';
export * from './createAssociatedTokenAccount.js';
export * from './createMint.js';
export * from './createMultisig.js';
export * from './createNativeMint.js';
export * from './createWrappedNativeAccount.js';
export * from './freezeAccount.js';
export * from './getOrCreateAssociatedTokenAccount.js';
export * from './mintTo.js';
export * from './mintToChecked.js';
export * from './revoke.js';
export * from './setAuthority.js';
export * from './mintTo.js';
export * from './burn.js';
export * from './closeAccount.js';
export * from './freezeAccount.js';
export * from './thawAccount.js';
export * from './transferChecked.js';
export * from './approveChecked.js';
export * from './mintToChecked.js';
export * from './burnChecked.js';
export * from './syncNative.js';
export * from './createAssociatedTokenAccount.js';
export * from './getOrCreateAssociatedTokenAccount.js';
export * from './thawAccount.js';
export * from './transfer.js';
export * from './transferChecked.js';
export * from './uiAmountToAmount.js';

View File

@ -0,0 +1,32 @@
import { u64 } from '@solana/buffer-layout-utils';
import type { Connection, PublicKey, Signer, TransactionError } from '@solana/web3.js';
import { Transaction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from '../constants.js';
import { createUiAmountToAmountInstruction } from '../instructions/uiAmountToAmount.js';
/**
* Amount as a string using mint-prescribed decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Mint for the account
* @param amount Ui Amount of tokens to be converted to Amount
* @param programId SPL Token program account
*
* @return Ui Amount generated
*/
export async function uiAmountToAmount(
connection: Connection,
payer: Signer,
mint: PublicKey,
amount: string,
programId = TOKEN_PROGRAM_ID
): Promise<bigint | TransactionError | null> {
const transaction = new Transaction().add(createUiAmountToAmountInstruction(mint, amount, programId));
const { returnData, err } = (await connection.simulateTransaction(transaction, [payer], false)).value;
if (returnData) {
const data = Buffer.from(returnData.data[0], returnData.data[1]);
return u64().decode(data);
}
return err;
}

View File

@ -1,9 +1,9 @@
export * from './accountType.js';
export * from './defaultAccountState/index.js';
export * from './extensionType.js';
export * from './memoTransfer/index.js';
export * from './mintCloseAuthority.js';
export * from './immutableOwner.js';
export * from './interestBearingMint/index.js';
export * from './memoTransfer/index.js';
export * from './mintCloseAuthority.js';
export * from './nonTransferable.js';
export * from './transferFee/index.js';

View File

@ -1,6 +1,6 @@
export * from './extensions/index.js';
export * from './instructions/index.js';
export * from './state/index.js';
export * from './actions/index.js';
export * from './constants.js';
export * from './errors.js';
export * from './extensions/index.js';
export * from './instructions/index.js';
export * from './state/index.js';

View File

@ -0,0 +1,128 @@
import { struct, u8 } from '@solana/buffer-layout';
import { u64 } from '@solana/buffer-layout-utils';
import type { AccountMeta, PublicKey } from '@solana/web3.js';
import { TransactionInstruction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from '../constants.js';
import {
TokenInvalidInstructionDataError,
TokenInvalidInstructionKeysError,
TokenInvalidInstructionProgramError,
TokenInvalidInstructionTypeError,
} from '../errors.js';
import { TokenInstruction } from './types.js';
/** TODO: docs */
export interface AmountToUiAmountInstructionData {
instruction: TokenInstruction.AmountToUiAmount;
amount: bigint;
}
/** TODO: docs */
export const amountToUiAmountInstructionData = struct<AmountToUiAmountInstructionData>([
u8('instruction'),
u64('amount'),
]);
/**
* Construct a AmountToUiAmount instruction
*
* @param mint Public key of the mint
* @param amount Amount of tokens to be converted to UiAmount
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export function createAmountToUiAmountInstruction(
mint: PublicKey,
amount: number | bigint,
programId = TOKEN_PROGRAM_ID
): TransactionInstruction {
const keys = [{ pubkey: mint, isSigner: false, isWritable: false }];
const data = Buffer.alloc(amountToUiAmountInstructionData.span);
amountToUiAmountInstructionData.encode(
{
instruction: TokenInstruction.AmountToUiAmount,
amount: BigInt(amount),
},
data
);
return new TransactionInstruction({ keys, programId, data });
}
/** A decoded, valid AmountToUiAmount instruction */
export interface DecodedAmountToUiAmountInstruction {
programId: PublicKey;
keys: {
mint: AccountMeta;
};
data: {
instruction: TokenInstruction.AmountToUiAmount;
amount: bigint;
};
}
/**
* Decode a AmountToUiAmount instruction and validate it
*
* @param instruction Transaction instruction to decode
* @param programId SPL Token program account
*
* @return Decoded, valid instruction
*/
export function decodeAmountToUiAmountInstruction(
instruction: TransactionInstruction,
programId = TOKEN_PROGRAM_ID
): DecodedAmountToUiAmountInstruction {
if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError();
if (instruction.data.length !== amountToUiAmountInstructionData.span) throw new TokenInvalidInstructionDataError();
const {
keys: { mint },
data,
} = decodeAmountToUiAmountInstructionUnchecked(instruction);
if (data.instruction !== TokenInstruction.AmountToUiAmount) throw new TokenInvalidInstructionTypeError();
if (!mint) throw new TokenInvalidInstructionKeysError();
return {
programId,
keys: {
mint,
},
data,
};
}
/** A decoded, non-validated AmountToUiAmount instruction */
export interface DecodedAmountToUiAmountInstructionUnchecked {
programId: PublicKey;
keys: {
mint: AccountMeta | undefined;
};
data: {
instruction: number;
amount: bigint;
};
}
/**
* Decode a AmountToUiAmount instruction without validating it
*
* @param instruction Transaction instruction to decode
*
* @return Decoded, non-validated instruction
*/
export function decodeAmountToUiAmountInstructionUnchecked({
programId,
keys: [mint],
data,
}: TransactionInstruction): DecodedAmountToUiAmountInstructionUnchecked {
return {
programId,
keys: {
mint,
},
data: amountToUiAmountInstructionData.decode(data),
};
}

View File

@ -2,6 +2,8 @@ import { u8 } from '@solana/buffer-layout';
import type { TransactionInstruction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from '../constants.js';
import { TokenInvalidInstructionDataError, TokenInvalidInstructionTypeError } from '../errors.js';
import type { DecodedAmountToUiAmountInstruction } from './amountToUiAmount.js';
import { decodeAmountToUiAmountInstruction } from './amountToUiAmount.js';
import type { DecodedApproveInstruction } from './approve.js';
import { decodeApproveInstruction } from './approve.js';
import type { DecodedApproveCheckedInstruction } from './approveChecked.js';
@ -41,6 +43,8 @@ import { decodeTransferInstruction } from './transfer.js';
import type { DecodedTransferCheckedInstruction } from './transferChecked.js';
import { decodeTransferCheckedInstruction } from './transferChecked.js';
import { TokenInstruction } from './types.js';
import type { DecodedUiAmountToAmountInstruction } from './uiAmountToAmount.js';
import { decodeUiAmountToAmountInstruction } from './uiAmountToAmount.js';
/** TODO: docs */
export type DecodedInstruction =
@ -63,6 +67,8 @@ export type DecodedInstruction =
| DecodedInitializeAccount2Instruction
| DecodedSyncNativeInstruction
| DecodedInitializeAccount3Instruction
| DecodedAmountToUiAmountInstruction
| DecodedUiAmountToAmountInstruction
// | DecodedInitializeMultisig2Instruction
// | DecodedInitializeMint2Instruction
// TODO: implement ^ and remove `never`
@ -96,6 +102,8 @@ export function decodeInstruction(
if (type === TokenInstruction.InitializeAccount2)
return decodeInitializeAccount2Instruction(instruction, programId);
if (type === TokenInstruction.SyncNative) return decodeSyncNativeInstruction(instruction, programId);
if (type === TokenInstruction.AmountToUiAmount) return decodeAmountToUiAmountInstruction(instruction, programId);
if (type === TokenInstruction.UiAmountToAmount) return decodeUiAmountToAmountInstruction(instruction, programId);
// TODO: implement
if (type === TokenInstruction.InitializeAccount3)
return decodeInitializeAccount3Instruction(instruction, programId);
@ -212,6 +220,20 @@ export function isInitializeAccount3Instruction(
return decoded.data.instruction === TokenInstruction.InitializeAccount3;
}
/** TODO: docs */
export function isAmountToUiAmountInstruction(
decoded: DecodedInstruction
): decoded is DecodedAmountToUiAmountInstruction {
return decoded.data.instruction === TokenInstruction.AmountToUiAmount;
}
/** TODO: docs */
export function isUiamountToAmountInstruction(
decoded: DecodedInstruction
): decoded is DecodedUiAmountToAmountInstruction {
return decoded.data.instruction === TokenInstruction.UiAmountToAmount;
}
/** TODO: docs, implement */
// export function isInitializeMultisig2Instruction(
// decoded: DecodedInstruction

View File

@ -24,7 +24,9 @@ export * from './initializeAccount3.js'; // 18
export * from './initializeMultisig2.js'; // 19
export * from './initializeMint2.js'; // 20
export * from './initializeImmutableOwner.js'; // 22
export * from './initializeMintCloseAuthority.js'; // 23
export * from './amountToUiAmount.js'; // 23
export * from './uiAmountToAmount.js'; // 24
export * from './initializeMintCloseAuthority.js'; // 25
export * from './reallocate.js'; // 29
export * from './createNativeMint.js'; // 31
export * from './initializeNonTransferableMint.js'; // 32

View File

@ -0,0 +1,136 @@
import { blob, struct, u8 } from '@solana/buffer-layout';
import type { AccountMeta, PublicKey } from '@solana/web3.js';
import { TransactionInstruction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from '../constants.js';
import {
TokenInvalidInstructionDataError,
TokenInvalidInstructionKeysError,
TokenInvalidInstructionProgramError,
TokenInvalidInstructionTypeError,
} from '../errors.js';
import { TokenInstruction } from './types.js';
/** TODO: docs */
export interface UiAmountToAmountInstructionData {
instruction: TokenInstruction.UiAmountToAmount;
amount: Uint8Array;
}
/** TODO: docs */
/**
* Construct a UiAmountToAmount instruction
*
* @param mint Public key of the mint
* @param amount UiAmount of tokens to be converted to Amount
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export function createUiAmountToAmountInstruction(
mint: PublicKey,
amount: string,
programId = TOKEN_PROGRAM_ID
): TransactionInstruction {
const keys = [{ pubkey: mint, isSigner: false, isWritable: false }];
const buf = Buffer.from(amount, 'utf8');
const uiAmountToAmountInstructionData = struct<UiAmountToAmountInstructionData>([
u8('instruction'),
blob(buf.length, 'amount'),
]);
const data = Buffer.alloc(uiAmountToAmountInstructionData.span);
uiAmountToAmountInstructionData.encode(
{
instruction: TokenInstruction.UiAmountToAmount,
amount: buf,
},
data
);
return new TransactionInstruction({ keys, programId, data });
}
/** A decoded, valid UiAmountToAmount instruction */
export interface DecodedUiAmountToAmountInstruction {
programId: PublicKey;
keys: {
mint: AccountMeta;
};
data: {
instruction: TokenInstruction.UiAmountToAmount;
amount: Uint8Array;
};
}
/**
* Decode a UiAmountToAmount instruction and validate it
*
* @param instruction Transaction instruction to decode
* @param programId SPL Token program account
*
* @return Decoded, valid instruction
*/
export function decodeUiAmountToAmountInstruction(
instruction: TransactionInstruction,
programId = TOKEN_PROGRAM_ID
): DecodedUiAmountToAmountInstruction {
if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError();
const uiAmountToAmountInstructionData = struct<UiAmountToAmountInstructionData>([
u8('instruction'),
blob(instruction.data.length - 1, 'amount'),
]);
if (instruction.data.length !== uiAmountToAmountInstructionData.span) throw new TokenInvalidInstructionDataError();
const {
keys: { mint },
data,
} = decodeUiAmountToAmountInstructionUnchecked(instruction);
if (data.instruction !== TokenInstruction.UiAmountToAmount) throw new TokenInvalidInstructionTypeError();
if (!mint) throw new TokenInvalidInstructionKeysError();
return {
programId,
keys: {
mint,
},
data,
};
}
/** A decoded, non-validated UiAmountToAmount instruction */
export interface DecodedUiAmountToAmountInstructionUnchecked {
programId: PublicKey;
keys: {
mint: AccountMeta | undefined;
};
data: {
instruction: number;
amount: Uint8Array;
};
}
/**
* Decode a UiAmountToAmount instruction without validating it
*
* @param instruction Transaction instruction to decode
*
* @return Decoded, non-validated instruction
*/
export function decodeUiAmountToAmountInstructionUnchecked({
programId,
keys: [mint],
data,
}: TransactionInstruction): DecodedUiAmountToAmountInstructionUnchecked {
const uiAmountToAmountInstructionData = struct<UiAmountToAmountInstructionData>([
u8('instruction'),
blob(data.length - 1, 'amount'),
]);
return {
programId,
keys: {
mint,
},
data: uiAmountToAmountInstructionData.decode(data),
};
}

View File

@ -0,0 +1,41 @@
import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import type { Connection, PublicKey, Signer } from '@solana/web3.js';
import { Keypair } from '@solana/web3.js';
import { createMint, amountToUiAmount, uiAmountToAmount } from '../../src';
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
chai.use(chaiAsPromised);
const TEST_TOKEN_DECIMALS = 2;
describe('Amount', () => {
let connection: Connection;
let payer: Signer;
let mint: PublicKey;
let mintAuthority: Keypair;
before(async () => {
connection = await getConnection();
payer = await newAccountWithLamports(connection, 1000000000);
mintAuthority = Keypair.generate();
const mintKeypair = Keypair.generate();
mint = await createMint(
connection,
payer,
mintAuthority.publicKey,
mintAuthority.publicKey,
TEST_TOKEN_DECIMALS,
mintKeypair,
undefined,
TEST_PROGRAM_ID
);
});
it('amountToUiAmount', async () => {
const amount = BigInt(5245);
const uiAmount = await amountToUiAmount(connection, payer, mint, amount, TEST_PROGRAM_ID);
expect(uiAmount).to.eql('52.45');
});
it('uiAmountToAmount', async () => {
const uiAmount = await uiAmountToAmount(connection, payer, mint, '52.45', TEST_PROGRAM_ID);
expect(uiAmount).to.eql(BigInt(5245));
});
});

View File

@ -18,6 +18,8 @@ import {
getAssociatedTokenAddressSync,
createInitializeAccount2Instruction,
createInitializeAccount3Instruction,
createAmountToUiAmountInstruction,
createUiAmountToAmountInstruction,
} from '../../src';
chai.use(chaiAsPromised);
@ -114,6 +116,18 @@ describe('spl-token-2022 instructions', () => {
expect(ix.data[1]).to.eql(extensionTypes[0]);
expect(ix.data[3]).to.eql(extensionTypes[1]);
});
it('AmountToUiAmount', () => {
const ix = createAmountToUiAmountInstruction(Keypair.generate().publicKey, 22, TOKEN_2022_PROGRAM_ID);
expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID);
expect(ix.keys).to.have.length(1);
});
it('UiAmountToAmount', () => {
const ix = createUiAmountToAmountInstruction(Keypair.generate().publicKey, '22', TOKEN_2022_PROGRAM_ID);
expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID);
expect(ix.keys).to.have.length(1);
});
});
describe('spl-associated-token-account instructions', () => {