Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 056948b

Browse files
authored
token-js: Add memo transfer support (#3257)
1 parent 5c47856 commit 056948b

File tree

9 files changed

+329
-2
lines changed

9 files changed

+329
-2
lines changed

token/js/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@solana/web3.js": "^1.41.0"
4646
},
4747
"devDependencies": {
48+
"@solana/spl-memo": "^0.1.0",
4849
"@types/chai-as-promised": "^7.1.4",
4950
"@types/eslint": "^8.4.0",
5051
"@types/eslint-plugin-prettier": "^3.1.0",

token/js/src/extensions/extensionType.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ACCOUNT_SIZE } from '../state/account';
22
import { Mint, MINT_SIZE } from '../state/mint';
33
import { MULTISIG_SIZE } from '../state/multisig';
44
import { ACCOUNT_TYPE_SIZE } from './accountType';
5+
import { MEMO_TRANSFER_SIZE } from './memoTransfer';
56
import { DEFAULT_ACCOUNT_STATE_SIZE } from './defaultAccountState';
67
import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority';
78
import { IMMUTABLE_OWNER_SIZE } from './immutableOwner';
@@ -45,7 +46,7 @@ export function getTypeLen(e: ExtensionType): number {
4546
case ExtensionType.ImmutableOwner:
4647
return IMMUTABLE_OWNER_SIZE;
4748
case ExtensionType.MemoTransfer:
48-
return 1;
49+
return MEMO_TRANSFER_SIZE;
4950
case ExtensionType.NonTransferable:
5051
return NON_TRANSFERABLE_SIZE;
5152
default:

token/js/src/extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './accountType';
22
export * from './defaultAccountState/index';
33
export * from './extensionType';
4+
export * from './memoTransfer/index';
45
export * from './mintCloseAuthority';
56
export * from './immutableOwner';
67
export * from './nonTransferable';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {
2+
ConfirmOptions,
3+
Connection,
4+
PublicKey,
5+
sendAndConfirmTransaction,
6+
Signer,
7+
Transaction,
8+
TransactionSignature,
9+
} from '@solana/web3.js';
10+
import { TOKEN_2022_PROGRAM_ID } from '../../constants';
11+
import {
12+
createEnableRequiredMemoTransfersInstruction,
13+
createDisableRequiredMemoTransfersInstruction,
14+
} from './instructions';
15+
import { getSigners } from '../../actions/internal';
16+
17+
/**
18+
* Enable memo transfers on the given account
19+
*
20+
* @param connection Connection to use
21+
* @param payer Payer of the transaction fees
22+
* @param account Account to modify
23+
* @param owner Owner of the account
24+
* @param multiSigners Signing accounts if `owner` is a multisig
25+
* @param confirmOptions Options for confirming the transaction
26+
* @param programId SPL Token program account
27+
*
28+
* @return Signature of the confirmed transaction
29+
*/
30+
export async function enableRequiredMemoTransfers(
31+
connection: Connection,
32+
payer: Signer,
33+
account: PublicKey,
34+
owner: Signer | PublicKey,
35+
multiSigners: Signer[] = [],
36+
confirmOptions?: ConfirmOptions,
37+
programId = TOKEN_2022_PROGRAM_ID
38+
): Promise<TransactionSignature> {
39+
const [ownerPublicKey, signers] = getSigners(owner, multiSigners);
40+
41+
const transaction = new Transaction().add(
42+
createEnableRequiredMemoTransfersInstruction(account, ownerPublicKey, signers, programId)
43+
);
44+
45+
return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
46+
}
47+
48+
/**
49+
* Disable memo transfers on the given account
50+
*
51+
* @param connection Connection to use
52+
* @param payer Payer of the transaction fees
53+
* @param account Account to modify
54+
* @param owner Owner of the account
55+
* @param multiSigners Signing accounts if `owner` is a multisig
56+
* @param confirmOptions Options for confirming the transaction
57+
* @param programId SPL Token program account
58+
*
59+
* @return Signature of the confirmed transaction
60+
*/
61+
export async function disableRequiredMemoTransfers(
62+
connection: Connection,
63+
payer: Signer,
64+
account: PublicKey,
65+
owner: Signer | PublicKey,
66+
multiSigners: Signer[] = [],
67+
confirmOptions?: ConfirmOptions,
68+
programId = TOKEN_2022_PROGRAM_ID
69+
): Promise<TransactionSignature> {
70+
const [ownerPublicKey, signers] = getSigners(owner, multiSigners);
71+
72+
const transaction = new Transaction().add(
73+
createDisableRequiredMemoTransfersInstruction(account, ownerPublicKey, signers, programId)
74+
);
75+
76+
return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
77+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './actions';
2+
export * from './instructions';
3+
export * from './state';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { struct, u8 } from '@solana/buffer-layout';
2+
import { PublicKey, Signer, TransactionInstruction } from '@solana/web3.js';
3+
import { TokenInstruction } from '../../instructions/types';
4+
import { TOKEN_2022_PROGRAM_ID } from '../../constants';
5+
6+
export enum MemoTransferInstruction {
7+
Enable = 0,
8+
Disable = 1,
9+
}
10+
11+
/** TODO: docs */
12+
export interface MemoTransferInstructionData {
13+
instruction: TokenInstruction.MemoTransferExtension;
14+
memoTransferInstruction: MemoTransferInstruction;
15+
}
16+
17+
/** TODO: docs */
18+
export const memoTransferInstructionData = struct<MemoTransferInstructionData>([
19+
u8('instruction'),
20+
u8('memoTransferInstruction'),
21+
]);
22+
23+
/**
24+
* Construct an EnableRequiredMemoTransfers instruction
25+
*
26+
* @param account Token account to update
27+
* @param authority The account's owner/delegate
28+
* @param signers The signer account(s)
29+
* @param programId SPL Token program account
30+
*
31+
* @return Instruction to add to a transaction
32+
*/
33+
export function createEnableRequiredMemoTransfersInstruction(
34+
account: PublicKey,
35+
authority: PublicKey,
36+
multiSigners: Signer[] = [],
37+
programId = TOKEN_2022_PROGRAM_ID
38+
): TransactionInstruction {
39+
return createMemoTransferInstruction(/* enable */ true, account, authority, multiSigners, programId);
40+
}
41+
42+
/**
43+
* Construct a DisableMemoTransfer instruction
44+
*
45+
* @param account Token account to update
46+
* @param authority The account's owner/delegate
47+
* @param signers The signer account(s)
48+
* @param programId SPL Token program account
49+
*
50+
* @return Instruction to add to a transaction
51+
*/
52+
export function createDisableRequiredMemoTransfersInstruction(
53+
account: PublicKey,
54+
authority: PublicKey,
55+
multiSigners: Signer[] = [],
56+
programId = TOKEN_2022_PROGRAM_ID
57+
): TransactionInstruction {
58+
return createMemoTransferInstruction(/* enable */ false, account, authority, multiSigners, programId);
59+
}
60+
61+
function createMemoTransferInstruction(
62+
enable: boolean,
63+
account: PublicKey,
64+
authority: PublicKey,
65+
multiSigners: Signer[],
66+
programId: PublicKey
67+
): TransactionInstruction {
68+
const keys = [{ pubkey: account, isSigner: false, isWritable: true }];
69+
keys.push({ pubkey: authority, isSigner: !multiSigners.length, isWritable: false });
70+
for (const signer of multiSigners) {
71+
keys.push({ pubkey: signer.publicKey, isSigner: true, isWritable: false });
72+
}
73+
74+
const data = Buffer.alloc(memoTransferInstructionData.span);
75+
memoTransferInstructionData.encode(
76+
{
77+
instruction: TokenInstruction.MemoTransferExtension,
78+
memoTransferInstruction: enable ? MemoTransferInstruction.Enable : MemoTransferInstruction.Disable,
79+
},
80+
data
81+
);
82+
83+
return new TransactionInstruction({ keys, programId, data });
84+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { struct } from '@solana/buffer-layout';
2+
import { bool } from '@solana/buffer-layout-utils';
3+
import { Account } from '../../state';
4+
import { ExtensionType, getExtensionData } from '../extensionType';
5+
6+
/** MemoTransfer as stored by the program */
7+
export interface MemoTransfer {
8+
/** Require transfers into this account to be accompanied by a memo */
9+
requireIncomingTransferMemos: boolean;
10+
}
11+
12+
/** Buffer layout for de/serializing a transfer fee config extension */
13+
export const MemoTransferLayout = struct<MemoTransfer>([bool('requireIncomingTransferMemos')]);
14+
15+
export const MEMO_TRANSFER_SIZE = MemoTransferLayout.span;
16+
17+
export function getMemoTransfer(account: Account): MemoTransfer | null {
18+
const extensionData = getExtensionData(ExtensionType.MemoTransfer, account.tlvData);
19+
if (extensionData !== null) {
20+
return MemoTransferLayout.decode(extensionData);
21+
} else {
22+
return null;
23+
}
24+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import chai, { expect } from 'chai';
2+
import chaiAsPromised from 'chai-as-promised';
3+
chai.use(chaiAsPromised);
4+
5+
import {
6+
sendAndConfirmTransaction,
7+
Connection,
8+
Keypair,
9+
PublicKey,
10+
Signer,
11+
SystemProgram,
12+
Transaction,
13+
} from '@solana/web3.js';
14+
import { createMemoInstruction } from '@solana/spl-memo';
15+
import {
16+
createAccount,
17+
createMint,
18+
createEnableRequiredMemoTransfersInstruction,
19+
createInitializeAccountInstruction,
20+
createTransferInstruction,
21+
getAccount,
22+
getMemoTransfer,
23+
disableRequiredMemoTransfers,
24+
enableRequiredMemoTransfers,
25+
mintTo,
26+
transfer,
27+
getAccountLen,
28+
ExtensionType,
29+
} from '../../src';
30+
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
31+
32+
const TEST_TOKEN_DECIMALS = 2;
33+
const TRANSFER_AMOUNT = 1_000;
34+
const EXTENSIONS = [ExtensionType.MemoTransfer];
35+
describe('memoTransfer', () => {
36+
let connection: Connection;
37+
let payer: Signer;
38+
let owner: Keypair;
39+
let mint: PublicKey;
40+
let mintAuthority: Keypair;
41+
let source: PublicKey;
42+
let destination: PublicKey;
43+
before(async () => {
44+
connection = await getConnection();
45+
payer = await newAccountWithLamports(connection, 1000000000);
46+
mintAuthority = Keypair.generate();
47+
owner = Keypair.generate();
48+
});
49+
beforeEach(async () => {
50+
const mintKeypair = Keypair.generate();
51+
mint = await createMint(
52+
connection,
53+
payer,
54+
mintAuthority.publicKey,
55+
mintAuthority.publicKey,
56+
TEST_TOKEN_DECIMALS,
57+
mintKeypair,
58+
undefined,
59+
TEST_PROGRAM_ID
60+
);
61+
62+
source = await createAccount(
63+
connection,
64+
payer,
65+
mint,
66+
owner.publicKey,
67+
undefined, // uses ATA by default
68+
undefined,
69+
TEST_PROGRAM_ID
70+
);
71+
72+
const destinationKeypair = Keypair.generate();
73+
destination = destinationKeypair.publicKey;
74+
const accountLen = getAccountLen(EXTENSIONS);
75+
const lamports = await connection.getMinimumBalanceForRentExemption(accountLen);
76+
77+
const transaction = new Transaction().add(
78+
SystemProgram.createAccount({
79+
fromPubkey: payer.publicKey,
80+
newAccountPubkey: destination,
81+
space: accountLen,
82+
lamports,
83+
programId: TEST_PROGRAM_ID,
84+
}),
85+
createInitializeAccountInstruction(destination, mint, owner.publicKey, TEST_PROGRAM_ID),
86+
createEnableRequiredMemoTransfersInstruction(destination, owner.publicKey, [], TEST_PROGRAM_ID)
87+
);
88+
89+
await sendAndConfirmTransaction(connection, transaction, [payer, owner, destinationKeypair], undefined);
90+
await mintTo(
91+
connection,
92+
payer,
93+
mint,
94+
source,
95+
mintAuthority,
96+
TRANSFER_AMOUNT * 10,
97+
[],
98+
undefined,
99+
TEST_PROGRAM_ID
100+
);
101+
});
102+
it('fails without memo when enabled', async () => {
103+
const accountInfo = await getAccount(connection, destination, undefined, TEST_PROGRAM_ID);
104+
const memoTransfer = getMemoTransfer(accountInfo);
105+
expect(memoTransfer).to.not.be.null;
106+
if (memoTransfer !== null) {
107+
expect(memoTransfer.requireIncomingTransferMemos).to.be.true;
108+
}
109+
expect(transfer(connection, payer, source, destination, owner, TRANSFER_AMOUNT, [], undefined, TEST_PROGRAM_ID))
110+
.to.be.rejected;
111+
});
112+
it('works without memo when disabled', async () => {
113+
await disableRequiredMemoTransfers(connection, payer, destination, owner, [], undefined, TEST_PROGRAM_ID);
114+
await transfer(connection, payer, source, destination, owner, TRANSFER_AMOUNT, [], undefined, TEST_PROGRAM_ID);
115+
await enableRequiredMemoTransfers(connection, payer, destination, owner, [], undefined, TEST_PROGRAM_ID);
116+
expect(transfer(connection, payer, source, destination, owner, TRANSFER_AMOUNT, [], undefined, TEST_PROGRAM_ID))
117+
.to.be.rejected;
118+
});
119+
it('works with memo when enabled', async () => {
120+
const transaction = new Transaction().add(
121+
createMemoInstruction('transfer with a memo', [payer.publicKey, owner.publicKey]),
122+
createTransferInstruction(source, destination, owner.publicKey, TRANSFER_AMOUNT, [], TEST_PROGRAM_ID)
123+
);
124+
await sendAndConfirmTransaction(connection, transaction, [payer, owner], {
125+
preflightCommitment: 'confirmed',
126+
});
127+
});
128+
});

token/js/yarn.lock

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@
143143
dependencies:
144144
buffer "~6.0.3"
145145

146+
"@solana/spl-memo@^0.1.0":
147+
version "0.1.0"
148+
resolved "https://registry.yarnpkg.com/@solana/spl-memo/-/spl-memo-0.1.0.tgz#2578a48da5184b306195bef0d658dec532b46c74"
149+
integrity sha512-2Ob89TYXVkLXsdRIsVh9H/9rJrVXpOTapn/f/32n5/B8JRaSRBcVBLIAgAwSjMH1oJdBlmBO0lt8+cbWBrRLuA==
150+
dependencies:
151+
"@solana/web3.js" "^1.41.0"
152+
buffer "^6.0.3"
153+
146154
"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.41.0":
147155
version "1.41.10"
148156
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.41.10.tgz#fb1bf7d8ca25f126a2166fed1733fe357298a076"
@@ -595,7 +603,7 @@ [email protected]:
595603
base64-js "^1.3.1"
596604
ieee754 "^1.2.1"
597605

598-
buffer@~6.0.3:
606+
buffer@^6.0.3, buffer@~6.0.3:
599607
version "6.0.3"
600608
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
601609
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==

0 commit comments

Comments
 (0)