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:
parent
b06d1be6cb
commit
aef1e239b3
|
@ -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| {
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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, []);
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue