Skip to content

Commit

Permalink
Add support for native token (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
jackcmay authored Jul 10, 2020
1 parent c60b4cc commit ab5ea5c
Show file tree
Hide file tree
Showing 9 changed files with 595 additions and 39 deletions.
36 changes: 31 additions & 5 deletions token/inc/token.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ typedef enum Token_TokenInstruction_Tag {
*/
InitializeMint,
/**
* Initializes a new account to hold tokens.
* Initializes a new account to hold tokens. If this account is associated with the native mint
* then the token balance of the initialized account will be equal to the amount of SOL in the account.
*
* The `InitializeAccount` instruction requires no signers and MUST be included within
* the same Transaction as the system program's `CreateInstruction` that creates the account
Expand Down Expand Up @@ -75,7 +76,9 @@ typedef enum Token_TokenInstruction_Tag {
*/
InitializeMultisig,
/**
* Transfers tokens from one account to another either directly or via a delegate.
* Transfers tokens from one account to another either directly or via a delegate. If this
* account is associated with the native mint then equal amounts of SOL and Tokens will be
* transferred to the destination account.
*
* Accounts expected by this instruction:
*
Expand Down Expand Up @@ -141,7 +144,7 @@ typedef enum Token_TokenInstruction_Tag {
*/
SetOwner,
/**
* Mints new tokens to an account.
* Mints new tokens to an account. The native mint does not support minting.
*
* Accounts expected by this instruction:
*
Expand All @@ -158,7 +161,8 @@ typedef enum Token_TokenInstruction_Tag {
*/
MintTo,
/**
* Burns tokens by removing them from an account and thus circulation.
* Burns tokens by removing them from an account. `Burn` does not support accounts
* associated with the native mint, use `CloseAccount` instead.
*
* Accounts expected by this instruction:
*
Expand All @@ -168,10 +172,28 @@ typedef enum Token_TokenInstruction_Tag {
*
* * Multisignature owner/delegate
* 0. `[writable]` The account to burn from.
* 1. `[]` The account's multisignature owner/delegate
* 1. `[]` The account's multisignature owner/delegate.
* 2. ..2+M '[signer]' M signer accounts.
*/
Burn,
/**
* Close an account by transferring all its SOL to the destination account.
* Non-native accounts may only be closed if its token amount is zero.
*
* Accounts expected by this instruction:
*
* * Single owner/delegate
* 0. `[writable]` The account to close.
* 1. '[writable]' The destination account.
* 2. `[signer]` The account's owner.
*
* * Multisignature owner/delegate
* 0. `[writable]` The account to close.
* 1. '[writable]' The destination account.
* 2. `[]` The account's multisignature owner.
* 3. ..3+M '[signer]' M signer accounts.
*/
CloseAccount,
} Token_TokenInstruction_Tag;

typedef struct Token_InitializeMint_Body {
Expand Down Expand Up @@ -304,6 +326,10 @@ typedef struct Token_Account {
* Is `true` if this structure has been initialized
*/
bool is_initialized;
/**
* Is this a native token
*/
bool is_native;
/**
* The amount delegated
*/
Expand Down
12 changes: 9 additions & 3 deletions token/js/cli/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import {
failOnApproveOverspend,
setOwner,
mintTo,
burn,
multisig,
burn,
closeAccount,
nativeToken,
} from './token-test';

async function main() {
Expand All @@ -37,10 +39,14 @@ async function main() {
await setOwner();
console.log('Run test: mintTo');
await mintTo();
console.log('Run test: burn');
await burn();
console.log('Run test: multisig');
await multisig();
console.log('Run test: burn');
await burn();
console.log('Run test: closeAccount');
await closeAccount();
console.log('Run test: nativeToken');
await nativeToken();
console.log('Success\n');
}

Expand Down
72 changes: 72 additions & 0 deletions token/js/cli/token-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,75 @@ export async function multisig(): Promise<void> {
assert(accountInfo.owner.equals(newOwner));
}
}

export async function closeAccount(): Promise<void> {
const connection = await getConnection();
const owner = new Account();
const close = await testToken.createAccount(owner.publicKey);

let close_balance;
let info = await connection.getAccountInfo(close);
if (info != null) {
close_balance = info.lamports;
} else {
throw new Error('Account not found');
}

const balanceNeeded =
await connection.getMinimumBalanceForRentExemption(0);
const dest = await newAccountWithLamports(connection, balanceNeeded);

info = await connection.getAccountInfo(dest.publicKey);
if (info != null) {
assert(info.lamports == balanceNeeded);
} else {
throw new Error('Account not found');
}

await testToken.closeAccount(close, dest.publicKey, owner, []);
info = await connection.getAccountInfo(close);
if (info != null) {
throw new Error('Account not closed');
}
info = await connection.getAccountInfo(dest.publicKey);
if (info != null) {
assert(info.lamports == balanceNeeded + close_balance);
} else {
throw new Error('Account not found');
}
}

export async function nativeToken(): Promise<void> {
const connection = await getConnection();

const mintPublicKey = new PublicKey('So11111111111111111111111111111111111111111');
const payer = await newAccountWithLamports(connection, 100000000000 /* wag */);
const token = new Token(connection, mintPublicKey, programId, payer);
const owner = new Account();
const native = await token.createAccount(owner.publicKey);
let accountInfo = await token.getAccountInfo(native);
assert(accountInfo.isNative);
let balance;
let info = await connection.getAccountInfo(native);
if (info != null) {
balance = info.lamports;
} else {
throw new Error('Account not found');
}

const balanceNeeded =
await connection.getMinimumBalanceForRentExemption(0);
const dest = await newAccountWithLamports(connection, balanceNeeded);
await token.closeAccount(native, dest.publicKey, owner, []);
info = await connection.getAccountInfo(native);
if (info != null) {
throw new Error('Account not burned');
}
info = await connection.getAccountInfo(dest.publicKey);
if (info != null) {
assert(info.lamports == balanceNeeded + balance);
} else {
throw new Error('Account not found');
}

}
106 changes: 98 additions & 8 deletions token/js/client/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@ type MintInfo = {|
* Owner of the mint, given authority to mint new tokens
*/
owner: null | PublicKey,

/**
* Number of base 10 digits to the right of the decimal place
*/
decimals: number,

/**
* Is this mint initialized
*/
initialized: Boolean,
initialized: boolean,
|};

const MintLayout = BufferLayout.struct([
Expand Down Expand Up @@ -107,6 +109,16 @@ type AccountInfo = {|
* The amount of tokens the delegate authorized to the delegate
*/
delegatedAmount: TokenAmount,

/**
* Is this account initialized
*/
isInitialized: boolean,

/**
* Is this a native token account
*/
isNative: boolean,
|};

/**
Expand All @@ -119,7 +131,7 @@ const AccountLayout = BufferLayout.struct([
BufferLayout.u32('option'),
Layout.publicKey('delegate'),
BufferLayout.u8('is_initialized'),
BufferLayout.u8('padding'),
BufferLayout.u8('is_native'),
BufferLayout.u16('padding'),
Layout.uint64('delegatedAmount'),
]);
Expand All @@ -139,6 +151,11 @@ type MultisigInfo = {|
*/
n: number,

/**
* Is this mint initialized
*/
initialized: boolean,

/**
* The signers
*/
Expand Down Expand Up @@ -512,6 +529,8 @@ export class Token {
accountInfo.mint = new PublicKey(accountInfo.mint);
accountInfo.owner = new PublicKey(accountInfo.owner);
accountInfo.amount = TokenAmount.fromBuffer(accountInfo.amount);
accountInfo.isInitialized = accountInfo.isInitialized != 0;
accountInfo.isNative = accountInfo.isNative != 0;
if (accountInfo.option === 0) {
accountInfo.delegate = null;
accountInfo.delegatedAmount = new TokenAmount();
Expand Down Expand Up @@ -592,7 +611,7 @@ export class Token {
signers = multiSigners;
}
return await sendAndConfirmTransaction(
'transfer',
'Transfer',
this.connection,
new Transaction().add(
this.transferInstruction(
Expand Down Expand Up @@ -634,7 +653,7 @@ export class Token {
signers = multiSigners;
}
await sendAndConfirmTransaction(
'approve',
'Approve',
this.connection,
new Transaction().add(
this.approveInstruction(account, delegate, ownerPublicKey, multiSigners, amount),
Expand Down Expand Up @@ -666,7 +685,7 @@ export class Token {
signers = multiSigners;
}
await sendAndConfirmTransaction(
'revoke',
'Revoke',
this.connection,
new Transaction().add(
this.revokeInstruction(account, ownerPublicKey, multiSigners),
Expand Down Expand Up @@ -700,7 +719,7 @@ export class Token {
signers = multiSigners;
}
await sendAndConfirmTransaction(
'setOwneer',
'SetOwner',
this.connection,
new Transaction().add(
this.setOwnerInstruction(owned, newOwner, ownerPublicKey, multiSigners),
Expand Down Expand Up @@ -735,7 +754,7 @@ export class Token {
signers = multiSigners;
}
await sendAndConfirmTransaction(
'mintTo',
'MintTo',
this.connection,
new Transaction().add(this.mintToInstruction(dest, ownerPublicKey, multiSigners, amount)),
this.payer,
Expand Down Expand Up @@ -767,14 +786,45 @@ export class Token {
signers = multiSigners;
}
await sendAndConfirmTransaction(
'burn',
'Burn',
this.connection,
new Transaction().add(this.burnInstruction(account, ownerPublicKey, multiSigners, amount)),
this.payer,
...signers,
);
}

/**
* Burn account
*
* @param account Account to burn
* @param authority account owner
* @param multiSigners Signing accounts if `owner` is a multiSig
*/
async closeAccount(
account: PublicKey,
dest: PublicKey,
owner: Account | PublicKey,
multiSigners: Array<Account>,
): Promise<void> {
let ownerPublicKey;
let signers;
if (owner instanceof Account) {
ownerPublicKey = owner.publicKey;
signers = [owner];
} else {
ownerPublicKey = owner;
signers = multiSigners;
}
await sendAndConfirmTransaction(
'CloseAccount',
this.connection,
new Transaction().add(this.closeAccountInstruction(account, dest, ownerPublicKey, multiSigners)),
this.payer,
...signers,
);
}

/**
* Construct a Transfer instruction
*
Expand Down Expand Up @@ -1044,4 +1094,44 @@ export class Token {
data,
});
}

/**
* Construct a Burn instruction
*
* @param account Account to burn tokens from
* @param owner account owner
* @param multiSigners Signing accounts if `owner` is a multiSig
*/
closeAccountInstruction(
account: PublicKey,
dest: PublicKey,
owner: Account | PublicKey,
multiSigners: Array<Account>,
): TransactionInstruction {
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 9, // CloseAccount instruction
},
data,
);

let keys = [
{pubkey: account, isSigner: false, isWritable: true},
{pubkey: dest, isSigner: false, isWritable: true},
];
if (owner instanceof Account) {
keys.push({pubkey: owner.publicKey, isSigner: true, isWritable: false});
} else {
keys.push({pubkey: owner, isSigner: false, isWritable: false});
multiSigners.forEach(signer => keys.push({pubkey: signer.publicKey, isSigner: true, isWritable: false}));
}

return new TransactionInstruction({
keys,
programId: this.programId,
data,
});
}
}
Loading

0 comments on commit ab5ea5c

Please sign in to comment.