Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for native token #112

Merged
merged 4 commits into from
Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions token/inc/token.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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 @@ -71,7 +72,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 @@ -137,7 +140,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 @@ -154,7 +157,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 @@ -164,10 +168,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 @@ -300,6 +322,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