token: Add SyncNative instruction (program, CLI, JS) (#2091)

* Add SyncNative to program

* Add CLI support

* Add JS bindings

* Fix syncNative test to not run for existing token program

* Combine checks
This commit is contained in:
Jon Cinque 2021-07-19 21:50:42 +02:00 committed by GitHub
parent b06d1be6cb
commit aef1e239b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 359 additions and 3 deletions

View File

@ -29,15 +29,31 @@ impl<'a> Config<'a> {
arg_matches: &ArgMatches,
override_name: &str,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Pubkey {
let token = pubkey_of_signer(arg_matches, "token", wallet_manager).unwrap();
self.associated_token_address_for_token_or_override(
arg_matches,
override_name,
wallet_manager,
token,
)
}
// Check if an explicit token account address was provided, otherwise
// return the associated token address for the default address.
pub(crate) fn associated_token_address_for_token_or_override(
&self,
arg_matches: &ArgMatches,
override_name: &str,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
token: Option<Pubkey>,
) -> Pubkey {
if let Some(address) = pubkey_of_signer(arg_matches, override_name, wallet_manager).unwrap()
{
return address;
}
let token = pubkey_of_signer(arg_matches, "token", wallet_manager)
.unwrap()
.unwrap();
let token = token.unwrap();
let owner = self
.default_signer(arg_matches, wallet_manager)
.unwrap_or_else(|e| {

View File

@ -1336,6 +1336,11 @@ fn command_gc(config: &Config, owner: Pubkey) -> CommandResult {
Ok(Some((lamports_needed, instructions)))
}
fn command_sync_native(native_account_address: Pubkey) -> CommandResult {
let instructions = vec![sync_native(&spl_token::id(), &native_account_address)?];
Ok(Some((0, vec![instructions])))
}
struct SignOnlyNeedsFullMintSpec {}
impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec {
fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> {
@ -2105,6 +2110,27 @@ fn main() {
.about("Cleanup unnecessary token accounts")
.arg(owner_keypair_arg())
)
.subcommand(
SubCommand::with_name("sync-native")
.about("Sync a native SOL token account to its underlying lamports")
.arg(
owner_address_arg()
.index(1)
.conflicts_with("address")
.help("Owner of the associated account for the native token. \
To query a specific account, use the `--address` parameter instead. \
Defaults to the client keypair."),
)
.arg(
Arg::with_name("address")
.validator(is_valid_pubkey)
.value_name("TOKEN_ACCOUNT_ADDRESS")
.takes_value(true)
.long("address")
.conflicts_with("owner")
.help("Specify the specific token account address to sync"),
),
)
.get_matches();
let mut wallet_manager = None;
@ -2516,6 +2542,16 @@ fn main() {
command_gc(&config, owner_address)
}
("sync-native", Some(arg_matches)) => {
let address = config.associated_token_address_for_token_or_override(
arg_matches,
"address",
&mut wallet_manager,
Some(native_mint::id()),
);
command_sync_native(address)
}
_ => unreachable!(),
}
.and_then(|transaction_info| {

View File

@ -7,6 +7,8 @@ import {
BpfLoader,
PublicKey,
Signer,
SystemProgram,
Transaction,
BPF_LOADER_PROGRAM_ID,
} from '@solana/web3.js';
@ -18,6 +20,7 @@ import {
} from '../client/token';
import {url} from '../url';
import {newAccountWithLamports} from '../client/util/new-account-with-lamports';
import {sendAndConfirmTransaction} from '../client/util/send-and-confirm-transaction';
import {sleep} from '../client/util/sleep';
import {Store} from './store';
@ -630,6 +633,36 @@ export async function nativeToken(): Promise<void> {
throw new Error('Account not found');
}
const programVersion = process.env.PROGRAM_VERSION;
if (!programVersion) {
// transfer lamports into the native account
const additionalLamports = 100;
await sendAndConfirmTransaction(
'TransferLamports',
connection,
new Transaction().add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: native,
lamports: additionalLamports,
}),
),
payer,
);
// no change in the amount
accountInfo = await token.getAccountInfo(native);
assert(accountInfo.amount.toNumber() === lamportsToWrap);
// sync, amount changes
await token.syncNative(native);
accountInfo = await token.getAccountInfo(native);
assert(
accountInfo.amount.toNumber() === lamportsToWrap + additionalLamports,
);
balance += additionalLamports;
}
const balanceNeeded = await connection.getMinimumBalanceForRentExemption(0);
const dest = await newAccountWithLamports(connection, balanceNeeded);
await token.closeAccount(native, dest.publicKey, owner, []);

View File

@ -1397,6 +1397,22 @@ export class Token {
);
}
/**
* Sync amount in native SPL token account to underlying lamports
*
* @param nativeAccount Account to sync
*/
async syncNative(nativeAccount: PublicKey): Promise<void> {
await sendAndConfirmTransaction(
'SyncNative',
this.connection,
new Transaction().add(
Token.createSyncNativeInstruction(this.programId, nativeAccount),
),
this.payer,
);
}
/**
* Construct an InitializeMint instruction
*
@ -2225,6 +2241,30 @@ export class Token {
});
}
/**
* Construct a SyncNative instruction
*
* @param programId SPL Token program account
* @param nativeAccount Account to sync lamports from
*/
static createSyncNativeInstruction(
programId: PublicKey,
nativeAccount: PublicKey,
): TransactionInstruction {
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 17, // SyncNative instruction
},
data,
);
let keys = [{pubkey: nativeAccount, isSigner: false, isWritable: true}];
return new TransactionInstruction({keys, programId: programId, data});
}
/**
* Get the address for the associated token account
*

View File

@ -147,6 +147,13 @@ declare module '@solana/spl-token' {
multiSigners: Array<Signer>,
amount: number | u64,
): Promise<void>;
burnChecked(
account: PublicKey,
owner: any,
multiSigners: Array<Signer>,
amount: number | u64,
decimals: number,
): Promise<void>;
freezeAccount(
account: PublicKey,
authority: any,
@ -163,6 +170,7 @@ declare module '@solana/spl-token' {
authority: Signer | PublicKey,
multiSigners: Array<Signer>,
): Promise<void>;
syncNative(nativeAccount: PublicKey): Promise<void>;
static createInitMintInstruction(
programId: PublicKey,
mint: PublicKey,
@ -243,6 +251,19 @@ declare module '@solana/spl-token' {
authority: PublicKey,
multiSigners: Array<Signer>,
): TransactionInstruction;
static createBurnCheckedInstruction(
programId: PublicKey,
mint: PublicKey,
account: PublicKey,
owner: PublicKey,
multiSigners: Array<Signer>,
amount: number | u64,
decimals: number,
): TransactionInstruction;
static createSyncNativeInstruction(
programId: PublicKey,
nativeAccount: PublicKey,
): TransactionInstruction;
static createAssociatedTokenAccountInstruction(
associatedProgramId: PublicKey,
programId: PublicKey,

View File

@ -76,4 +76,13 @@ describe('Token', () => {
expect(ix.programId).to.eql(ASSOCIATED_TOKEN_PROGRAM_ID);
expect(ix.keys).to.have.length(7);
});
it('syncNative', () => {
const ix = Token.createSyncNativeInstruction(
TOKEN_PROGRAM_ID,
Keypair.generate().publicKey,
);
expect(ix.programId).to.eql(TOKEN_PROGRAM_ID);
expect(ix.keys).to.have.length(1);
});
});

View File

@ -7,6 +7,7 @@ use thiserror::Error;
/// Errors that may be returned by the Token program.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum TokenError {
// 0
/// Lamport balance below rent-exempt threshold.
#[error("Lamport balance below rent-exempt threshold")]
NotRentExempt,
@ -22,6 +23,8 @@ pub enum TokenError {
/// Owner does not match.
#[error("Owner does not match")]
OwnerMismatch,
// 5
/// This token's supply is fixed and new tokens cannot be minted.
#[error("Fixed supply")]
FixedSupply,
@ -37,6 +40,8 @@ pub enum TokenError {
/// State is uninitialized.
#[error("State is unititialized")]
UninitializedState,
// 10
/// Instruction does not support native tokens
#[error("Instruction does not support native tokens")]
NativeNotSupported,
@ -52,6 +57,8 @@ pub enum TokenError {
/// Operation overflowed
#[error("Operation overflowed")]
Overflow,
// 15
/// Account does not support specified authority type.
#[error("Account does not support specified authority type")]
AuthorityTypeNotSupported,
@ -64,6 +71,9 @@ pub enum TokenError {
/// Mint decimals mismatch between the client and mint
#[error("The provided decimals value different from the Mint decimals")]
MintDecimalsMismatch,
/// Instruction does not support non-native tokens
#[error("Instruction does not support non-native tokens")]
NonNativeNotSupported,
}
impl From<TokenError> for ProgramError {
fn from(e: TokenError) -> Self {

View File

@ -363,6 +363,16 @@ pub enum TokenInstruction {
/// The new account's owner/multisignature.
owner: Pubkey,
},
/// Given a wrapped / native token account (a token account containing SOL)
/// updates its amount field based on the account's underlying `lamports`.
/// This is useful if a non-wrapped SOL account uses `system_instruction::transfer`
/// to move lamports to a wrapped token account, and needs to have its token
/// `amount` field updated.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` The native token account to sync with its underlying lamports.
SyncNative,
}
impl TokenInstruction {
/// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html).
@ -464,6 +474,7 @@ impl TokenInstruction {
let (owner, _rest) = Self::unpack_pubkey(rest)?;
Self::InitializeAccount2 { owner }
}
17 => Self::SyncNative,
_ => return Err(TokenError::InvalidInstruction.into()),
})
@ -540,6 +551,9 @@ impl TokenInstruction {
buf.push(16);
buf.extend_from_slice(owner.as_ref());
}
&Self::SyncNative => {
buf.push(17);
}
};
buf
}
@ -1119,6 +1133,20 @@ pub fn burn_checked(
})
}
/// Creates a `SyncNative` instruction
pub fn sync_native(
token_program_id: &Pubkey,
account_pubkey: &Pubkey,
) -> Result<Instruction, ProgramError> {
check_program_account(token_program_id)?;
Ok(Instruction {
program_id: *token_program_id,
accounts: vec![AccountMeta::new(*account_pubkey, false)],
data: TokenInstruction::SyncNative.pack(),
})
}
/// Utility function that checks index is between MIN_SIGNERS and MAX_SIGNERS
pub fn is_valid_signer_index(index: usize) -> bool {
(MIN_SIGNERS..=MAX_SIGNERS).contains(&index)
@ -1288,5 +1316,12 @@ mod test {
assert_eq!(packed, expect);
let unpacked = TokenInstruction::unpack(&expect).unwrap();
assert_eq!(unpacked, check);
let check = TokenInstruction::SyncNative;
let packed = check.pack();
let expect = vec![17u8];
assert_eq!(packed, expect);
let unpacked = TokenInstruction::unpack(&expect).unwrap();
assert_eq!(unpacked, check);
}
}

View File

@ -644,6 +644,33 @@ impl Processor {
Ok(())
}
/// Processes a [SyncNative](enum.TokenInstruction.html) instruction
pub fn process_sync_native(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let native_account_info = next_account_info(account_info_iter)?;
if native_account_info.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut native_account = Account::unpack(&native_account_info.data.borrow())?;
if let COption::Some(rent_exempt_reserve) = native_account.is_native {
let new_amount = native_account_info
.lamports()
.checked_sub(rent_exempt_reserve)
.ok_or(TokenError::Overflow)?;
if new_amount < native_account.amount {
return Err(TokenError::InvalidState.into());
}
native_account.amount = new_amount;
} else {
return Err(TokenError::NonNativeNotSupported.into());
}
Account::pack(native_account, &mut native_account_info.data.borrow_mut())?;
Ok(())
}
/// Processes an [Instruction](enum.Instruction.html).
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let instruction = TokenInstruction::unpack(input)?;
@ -724,6 +751,10 @@ impl Processor {
msg!("Instruction: BurnChecked");
Self::process_burn(program_id, accounts, amount, Some(decimals))
}
TokenInstruction::SyncNative => {
msg!("Instruction: SyncNative");
Self::process_sync_native(program_id, accounts)
}
}
}
@ -802,6 +833,9 @@ impl PrintProgramError for TokenError {
TokenError::MintDecimalsMismatch => {
msg!("Error: decimals different from the Mint decimals")
}
TokenError::NonNativeNotSupported => {
msg!("Error: Instruction does not support non-native tokens")
}
}
}
}
@ -5775,4 +5809,126 @@ mod tests {
assert_eq!(account_account, account2_account);
}
#[test]
fn test_sync_native() {
let program_id = crate::id();
let mint_key = Pubkey::new_unique();
let mut mint_account =
SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id);
let native_account_key = Pubkey::new_unique();
let lamports = 40;
let mut native_account = SolanaAccount::new(
account_minimum_balance() + lamports,
Account::get_packed_len(),
&program_id,
);
let non_native_account_key = Pubkey::new_unique();
let mut non_native_account = SolanaAccount::new(
account_minimum_balance() + 50,
Account::get_packed_len(),
&program_id,
);
let owner_key = Pubkey::new_unique();
let mut owner_account = SolanaAccount::default();
let mut rent_sysvar = rent_sysvar();
// initialize non-native mint
do_process_instruction(
initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(),
vec![&mut mint_account, &mut rent_sysvar],
)
.unwrap();
// initialize non-native account
do_process_instruction(
initialize_account(&program_id, &non_native_account_key, &mint_key, &owner_key)
.unwrap(),
vec![
&mut non_native_account,
&mut mint_account,
&mut owner_account,
&mut rent_sysvar,
],
)
.unwrap();
let account = Account::unpack_unchecked(&non_native_account.data).unwrap();
assert!(!account.is_native());
assert_eq!(account.amount, 0);
// fail sync non-native
assert_eq!(
Err(TokenError::NonNativeNotSupported.into()),
do_process_instruction(
sync_native(&program_id, &non_native_account_key,).unwrap(),
vec![&mut non_native_account],
)
);
// fail sync uninitialized
assert_eq!(
Err(ProgramError::UninitializedAccount),
do_process_instruction(
sync_native(&program_id, &native_account_key,).unwrap(),
vec![&mut native_account],
)
);
// wrap native account
do_process_instruction(
initialize_account(
&program_id,
&native_account_key,
&crate::native_mint::id(),
&owner_key,
)
.unwrap(),
vec![
&mut native_account,
&mut mint_account,
&mut owner_account,
&mut rent_sysvar,
],
)
.unwrap();
let account = Account::unpack_unchecked(&native_account.data).unwrap();
assert!(account.is_native());
assert_eq!(account.amount, lamports);
// sync, no change
do_process_instruction(
sync_native(&program_id, &native_account_key).unwrap(),
vec![&mut native_account],
)
.unwrap();
let account = Account::unpack_unchecked(&native_account.data).unwrap();
assert_eq!(account.amount, lamports);
// transfer sol
let new_lamports = lamports + 50;
native_account.lamports = account_minimum_balance() + new_lamports;
// success sync
do_process_instruction(
sync_native(&program_id, &native_account_key).unwrap(),
vec![&mut native_account],
)
.unwrap();
let account = Account::unpack_unchecked(&native_account.data).unwrap();
assert_eq!(account.amount, new_lamports);
// reduce sol
native_account.lamports -= 1;
// fail sync
assert_eq!(
Err(TokenError::InvalidState.into()),
do_process_instruction(
sync_native(&program_id, &native_account_key,).unwrap(),
vec![&mut native_account],
)
);
}
}