Init
This commit is contained in:
commit
5ab454649c
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
.anchor
|
||||||
|
.DS_Store
|
||||||
|
target
|
||||||
|
**/*.rs.bk
|
|
@ -0,0 +1,9 @@
|
||||||
|
[registry]
|
||||||
|
url = "https://anchor.projectserum.com"
|
||||||
|
|
||||||
|
[provider]
|
||||||
|
cluster = "localnet"
|
||||||
|
wallet = "/home/armaniferrante/.config/solana/id.json"
|
||||||
|
|
||||||
|
[scripts]
|
||||||
|
test = "ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,4 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"programs/*"
|
||||||
|
]
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
// Migrations are an early feature. Currently, they're nothing more than this
|
||||||
|
// single deploy script that's invoked from the CLI, injecting a provider
|
||||||
|
// configured from the workspace's Anchor.toml.
|
||||||
|
|
||||||
|
const anchor = require("@project-serum/anchor");
|
||||||
|
|
||||||
|
module.exports = async function (provider) {
|
||||||
|
// Configure client to use the provider.
|
||||||
|
anchor.setProvider(provider);
|
||||||
|
|
||||||
|
// Add your deploy script here.
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@project-serum/anchor": "^0.13.2",
|
||||||
|
"bn.js": "^5.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/mocha": "^9.0.0",
|
||||||
|
"chai": "^4.3.4",
|
||||||
|
"mocha": "^9.0.3",
|
||||||
|
"ts-mocha": "^8.0.0",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "assert-balances"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Created with Anchor"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
name = "assert_balances"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
no-entrypoint = []
|
||||||
|
no-idl = []
|
||||||
|
cpi = ["no-entrypoint"]
|
||||||
|
default = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anchor-lang = "0.13.2"
|
||||||
|
anchor-spl = "0.13.2"
|
|
@ -0,0 +1,2 @@
|
||||||
|
[target.bpfel-unknown-unknown.dependencies.std]
|
||||||
|
features = []
|
|
@ -0,0 +1,82 @@
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use anchor_spl::token::{self, TokenAccount};
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
pub mod assert_balances {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Asserts the balances are greater than or equal to the amounts given.
|
||||||
|
///
|
||||||
|
/// This can be used as an additional safety check by wallets and other apps
|
||||||
|
/// to ensure the outcome of a given transaction is as expected. If a
|
||||||
|
/// transaction ends up changing balances more than expected, then it will
|
||||||
|
/// abort.
|
||||||
|
///
|
||||||
|
/// Each item in `expected_token_balances` corresponds with an account in
|
||||||
|
/// `ctx.remaining_accounts`.
|
||||||
|
pub fn assert(
|
||||||
|
ctx: Context<AssertBalances>,
|
||||||
|
expected_sol_balance: u64,
|
||||||
|
expected_token_balances: Vec<u64>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut expected_token_balances = expected_token_balances;
|
||||||
|
let mut accs = ctx.remaining_accounts;
|
||||||
|
|
||||||
|
// There must be a token account for each balance given.
|
||||||
|
if expected_token_balances.len() != accs.len() {
|
||||||
|
return Err(ErrorCode::InvalidBalanceLen.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert SOL balance.
|
||||||
|
if **ctx.accounts.user.lamports.borrow() < expected_sol_balance {
|
||||||
|
return Err(ErrorCode::InvalidSolBalance.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert token balances.
|
||||||
|
loop {
|
||||||
|
// Have we iterated through all tokens?
|
||||||
|
if accs.len() == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize the token account.
|
||||||
|
let token = CpiAccount::<TokenAccount>::try_accounts(ctx.program_id, &mut accs, &[])?;
|
||||||
|
|
||||||
|
// Is it owned by the SPL token program.
|
||||||
|
if token.to_account_info().owner != &token::ID {
|
||||||
|
return Err(ErrorCode::InvalidOwner.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it owned by the user?
|
||||||
|
if &token.owner != ctx.accounts.user.key {
|
||||||
|
return Err(ErrorCode::InvalidOwner.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the post balance at least the amount expected?
|
||||||
|
let balance = expected_token_balances.remove(0);
|
||||||
|
if token.amount < balance {
|
||||||
|
return Err(ErrorCode::InvalidTokenBalance.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct AssertBalances<'info> {
|
||||||
|
#[account(signer)]
|
||||||
|
user: AccountInfo<'info>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[error]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
#[msg("Owner doesn't match the SPL token program")]
|
||||||
|
InvalidOwner,
|
||||||
|
#[msg("Authority doesn't match the user")]
|
||||||
|
InvalidAuthority,
|
||||||
|
#[msg("Accounts length must match the given instruction balance length.")]
|
||||||
|
InvalidBalanceLen,
|
||||||
|
#[msg("Unexpected SOL balance")]
|
||||||
|
InvalidSolBalance,
|
||||||
|
#[msg("Unexpected token balance")]
|
||||||
|
InvalidTokenBalance,
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import * as anchor from '@project-serum/anchor';
|
||||||
|
import { Token, TOKEN_PROGRAM_ID, Token as MintClient, Account as TokenAccount } from '@solana/spl-token';
|
||||||
|
import { PublicKey } from '@solana/web3.js';
|
||||||
|
import BN from 'bn.js';
|
||||||
|
|
||||||
|
describe('assert-balances', () => {
|
||||||
|
|
||||||
|
anchor.setProvider(anchor.Provider.env());
|
||||||
|
const program = anchor.workspace.AssertBalances;
|
||||||
|
const mints: Array<MintAccounts> = [];
|
||||||
|
let ctx;
|
||||||
|
|
||||||
|
it('BOILERPLATE: Setup token accounts', async () => {
|
||||||
|
// Iniitialize each mint and token for the test suite.
|
||||||
|
for (let k = 0; k < 5; k += 1) {
|
||||||
|
// Create mint.
|
||||||
|
const client = await Token.createMint(
|
||||||
|
program.provider.connection,
|
||||||
|
program.provider.wallet.payer,
|
||||||
|
program.provider.wallet.publicKey,
|
||||||
|
null,
|
||||||
|
6,
|
||||||
|
TOKEN_PROGRAM_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create token account.
|
||||||
|
const tokenAddress = await client.createAccount(program.provider.wallet.publicKey);
|
||||||
|
|
||||||
|
// Mint to the account.
|
||||||
|
await client.mintTo(
|
||||||
|
tokenAddress,
|
||||||
|
program.provider.wallet.payer,
|
||||||
|
[],
|
||||||
|
1000000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the account data.
|
||||||
|
const tokenAccount = await client.getAccountInfo(tokenAddress);
|
||||||
|
|
||||||
|
// Save the mints.
|
||||||
|
mints.push({
|
||||||
|
client,
|
||||||
|
tokenAddress,
|
||||||
|
tokenAccount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared context for subsequent instructions.
|
||||||
|
ctx = {
|
||||||
|
accounts: {
|
||||||
|
user: program.provider.wallet.publicKey,
|
||||||
|
},
|
||||||
|
remainingAccounts: mints.map(m => {
|
||||||
|
return {
|
||||||
|
pubkey: m.tokenAddress, isWritable: false, isSigner: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Succeeds when balances are equal', async () => {
|
||||||
|
const solBalance = new BN('499999999982490600');
|
||||||
|
const tokenBalances = mints.map(mint => mint.tokenAccount.amount);
|
||||||
|
await program.rpc.assert(solBalance, tokenBalances, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Aborts balances when token accounts are not provided', async () => {
|
||||||
|
const solBalance = new BN('499999999982490600');
|
||||||
|
const tokenBalances = mints.map(mint => mint.tokenAccount.amount);
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await program.rpc.assert(solBalance, tokenBalances, {...ctx, remainingAccounts:[]});
|
||||||
|
}, (err) => {
|
||||||
|
// @ts-ignore
|
||||||
|
assert.ok(err.code, 302);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Fails to assert sol balance', async () => {
|
||||||
|
const solBalance = new BN('499999999982490599');
|
||||||
|
const tokenBalances = mints.map(mint => mint.tokenAccount.amount);
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await program.rpc.assert(solBalance, tokenBalances, ctx);
|
||||||
|
}, (err) => {
|
||||||
|
// @ts-ignore
|
||||||
|
assert.ok(err.code, 303);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Fails to assert token balance', async () => {
|
||||||
|
const solBalance = new BN('399999999982490600');
|
||||||
|
const tokenBalances = mints.map(mint => mint.tokenAccount.amount.addn(1));
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await program.rpc.assert(solBalance, tokenBalances, ctx);
|
||||||
|
}, (err) => {
|
||||||
|
// @ts-ignore
|
||||||
|
assert.ok(err.code, 304);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type MintAccounts = {
|
||||||
|
client: MintClient;
|
||||||
|
tokenAddress: PublicKey;
|
||||||
|
tokenAccount: TokenAccount;
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["mocha", "chai"],
|
||||||
|
"typeRoots": ["./node_modules/@types"],
|
||||||
|
"lib": ["es2015"],
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue