This commit is contained in:
armaniferrante 2021-09-01 12:05:45 -07:00
commit 5ab454649c
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
12 changed files with 2740 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
.anchor
.DS_Store
target
**/*.rs.bk

9
Anchor.toml Normal file
View File

@ -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"

1261
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

4
Cargo.toml Normal file
View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

13
migrations/deploy.ts Normal file
View File

@ -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.
}

13
package.json Normal file
View File

@ -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"
}
}

View File

@ -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"

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -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,
}

115
tests/assert-balances.ts Normal file
View File

@ -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;
};

10
tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true
}
}

1207
yarn.lock Normal file

File diff suppressed because it is too large Load Diff