Add spl mints; fix accepted token max; fix tsconfig; add buyer keypair

This commit is contained in:
Karl Kempe 2022-05-31 20:48:59 +00:00
parent e327eedc41
commit cbf5b061d3
9 changed files with 214 additions and 121 deletions

View File

@ -8,7 +8,7 @@ url = "https://anchor.projectserum.com"
[provider]
cluster = "localnet"
wallet = "./tests/test_keypair.json"
wallet = "./tests/test_orchestrator_keypair.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

View File

@ -26,7 +26,10 @@ pub const INIT_INDEX_SALE_END: usize = 100;
pub const INIT_INDEX_ACCEPTED_TOKENS_START: usize = 132;
pub const ACCEPTED_TOKENS_N_BYTES: usize = 33;
pub const ACCEPTED_TOKENS_MAX: usize = 10;
pub const ACCEPTED_TOKENS_MAX: usize = 8;
pub const ACCEPTED_TOKENS_INDEX_TOKEN_INDEX: usize = 0;
pub const ACCEPTED_TOKENS_INDEX_TOKEN_ADDRESS: usize = 1;
pub const ACCEPTED_TOKENS_INDEX_END: usize = 33;
// for attest contributions
pub const ATTEST_CONTRIBUTIONS_ELEMENT_LEN: usize = 33; // token index + amount

View File

@ -65,10 +65,15 @@ impl AssetTotal {
pub const MAXIMUM_SIZE: usize = 1 + 32 + 8 + 8 + 8;
pub fn make_from_slice(bytes: &[u8]) -> Result<Self> {
require!(bytes.len() == 33, SaleError::InvalidAcceptedTokenPayload);
require!(
bytes.len() == ACCEPTED_TOKENS_INDEX_END,
SaleError::InvalidAcceptedTokenPayload
);
Ok(AssetTotal {
token_index: bytes[0],
mint: Pubkey::new(&bytes[1..33]),
token_index: bytes[ACCEPTED_TOKENS_INDEX_TOKEN_INDEX],
mint: Pubkey::new(
&bytes[ACCEPTED_TOKENS_INDEX_TOKEN_ADDRESS..ACCEPTED_TOKENS_INDEX_END],
),
contributions: 0,
allocations: 0,
excess_contributions: 0,
@ -84,6 +89,7 @@ impl Sale {
+ (8 + 8)
+ 32
+ 1
+ 1
+ (4 + AssetTotal::MAXIMUM_SIZE * ACCEPTED_TOKENS_MAX)
+ 1;

View File

@ -1,15 +1,23 @@
import { AnchorProvider, workspace, web3, Program, setProvider } from "@project-serum/anchor";
import { AnchorProvider, workspace, web3, Program, setProvider, BN } from "@project-serum/anchor";
import { AnchorContributor } from "../target/types/anchor_contributor";
import { expect } from "chai";
import { readFileSync } from "fs";
import { CHAIN_ID_SOLANA, setDefaultWasm, tryHexToNativeString, tryNativeToHexString } from "@certusone/wormhole-sdk";
import { createAssociatedTokenAccount } from "@solana/spl-token";
import {
getOrCreateAssociatedTokenAccount,
getAssociatedTokenAddress,
createAssociatedTokenAccountInstruction,
createSyncNativeInstruction,
getAccount,
createMint,
getMint,
mintTo,
Account as AssociatedTokenAccount,
} from "@solana/spl-token";
import { DummyConductor } from "./helpers/conductor";
import { CONDUCTOR_ADDRESS, CONDUCTOR_CHAIN } from "./helpers/consts";
import { DummyConductor, MAX_ACCEPTED_TOKENS } from "./helpers/conductor";
import { IccoContributor } from "./helpers/contributor";
import { getBlockTime, wait } from "./helpers/utils";
import { BN } from "bn.js";
import { getBlockTime, getSplBalance, hexToPublicKey, wait } from "./helpers/utils";
setDefaultWasm("node");
@ -20,8 +28,11 @@ describe("anchor-contributor", () => {
const program = workspace.AnchorContributor as Program<AnchorContributor>;
const connection = program.provider.connection;
const owner = web3.Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync("./tests/test_keypair.json").toString()))
const orchestrator = web3.Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync("./tests/test_orchestrator_keypair.json").toString()))
);
const buyer = web3.Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync("./tests/test_buyer_keypair.json").toString()))
);
// TODO: we need other wallets for buyers
@ -32,6 +43,39 @@ describe("anchor-contributor", () => {
// our contributor
const contributor = new IccoContributor(program);
describe("Test Preparation", () => {
it("Create Dummy Sale Token", async () => {
// mint 8 unique tokens
const mint = await createMint(connection, orchestrator, orchestrator.publicKey, orchestrator.publicKey, 9);
// we need to simulate attesting the sale token on Solana.
// this allows us to "redeem" the sale token prior to sealing the sale
// (which in the case of this test means minting it on the contributor program's ATA)
await dummyConductor.attestSaleToken(connection, orchestrator);
});
it("Mint Accepted SPL Tokens to Buyer", async () => {
// first create them and add them to the accepted tokens list
const acceptedTokens = await dummyConductor.createAcceptedTokens(connection, orchestrator);
for (const token of acceptedTokens) {
const mint = new web3.PublicKey(tryHexToNativeString(token.address, CHAIN_ID_SOLANA));
const tokenAccount = await getOrCreateAssociatedTokenAccount(connection, orchestrator, mint, buyer.publicKey);
await mintTo(
connection,
orchestrator,
mint,
tokenAccount.address,
orchestrator,
20000000000n // 20,000,000,000 lamports
);
const balance = await getSplBalance(connection, mint, buyer.publicKey);
expect(balance).to.equal(20000000000n);
}
});
});
describe("Sanity Checks", () => {
it("Cannot Contribute to Non-Existent Sale", async () => {
{
@ -41,7 +85,7 @@ describe("anchor-contributor", () => {
let caughtError = false;
try {
const tx = await contributor.contribute(owner, saleId, tokenIndex, amount);
const tx = await contributor.contribute(orchestrator, saleId, tokenIndex, amount);
} catch (e) {
caughtError = e.error.errorCode.code == "AccountNotInitialized";
}
@ -55,24 +99,32 @@ describe("anchor-contributor", () => {
describe("Conduct Successful Sale", () => {
// contributor info
const contributionTokenIndex = 2;
const contributionAmounts = ["2000000000", "3000000000"];
const totalContributionAmount = contributionAmounts.map((x) => new BN(x)).reduce((prev, curr) => prev.add(curr));
const contributions = new Map<number, string[]>();
contributions.set(2, ["1200000000", "3400000000"]);
contributions.set(8, ["5600000000", "7800000000"]);
const totalContributions: BN[] = [];
contributions.forEach((amounts) => {
totalContributions.push(amounts.map((x) => new BN(x)).reduce((prev, curr) => prev.add(curr)));
});
// squirrel away associated sale token account
let saleTokenAccount: AssociatedTokenAccount;
it("Create ATA for Sale Token if Non-Existent", async () => {
saleTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
orchestrator,
dummyConductor.getSaleTokenOnSolana(),
program.programId
);
});
it("Orchestrator Initialize Sale with Signed VAA", async () => {
const tokenAccountKey = tryHexToNativeString(
"00000000000000000000000083752ecafebf4707258dedffbd9c7443148169db",
CHAIN_ID_SOLANA
); // placeholder
const startTime = 10 + (await getBlockTime(connection));
const duration = 5; // seconds
const initSaleVaa = dummyConductor.createSale(
await getBlockTime(connection),
duration,
new web3.PublicKey(tokenAccountKey)
);
const tx = await contributor.initSale(owner, initSaleVaa);
const initSaleVaa = dummyConductor.createSale(startTime, duration, saleTokenAccount.address);
const tx = await contributor.initSale(orchestrator, initSaleVaa);
{
// get the first sale state
@ -110,7 +162,7 @@ describe("anchor-contributor", () => {
it("Orchestrator Cannot Initialize Sale Again with Signed VAA", async () => {
let caughtError = false;
try {
const tx = await contributor.initSale(owner, dummyConductor.initSaleVaa);
const tx = await contributor.initSale(orchestrator, dummyConductor.initSaleVaa);
} catch (e) {
// pda init should fail
caughtError = "programErrorStack" in e;
@ -121,21 +173,6 @@ describe("anchor-contributor", () => {
}
});
it("Create Associated Token Accounts for Token Custodian", async () => {
// TODO: need to do sale token, too
const tokens = dummyConductor.acceptedTokens.map((token) => {
return tryHexToNativeString(token.address, CHAIN_ID_SOLANA)
});
/*
for(let addr of tokens) {
await createAssociatedTokenAccount(connection, owner, addr, program.programId);
}
*/
});
it("User Cannot Contribute Too Early", async () => {
const saleId = dummyConductor.getSaleId();
const tokenIndex = 2;
@ -143,7 +180,7 @@ describe("anchor-contributor", () => {
let caughtError = false;
try {
const tx = await contributor.contribute(owner, saleId, tokenIndex, amount);
const tx = await contributor.contribute(orchestrator, saleId, tokenIndex, amount);
} catch (e) {
caughtError = e.error.errorCode.code == "ContributionTooEarly";
}
@ -151,8 +188,6 @@ describe("anchor-contributor", () => {
if (!caughtError) {
throw Error("did not catch expected error");
}
// TODO: check balances on contract and buyer
});
it("User Contributes to Sale", async () => {
@ -163,37 +198,62 @@ describe("anchor-contributor", () => {
await wait(saleStart - blockTime + 1);
}
// now go about your business
const acceptedTokens = dummyConductor.acceptedTokens;
const startingBalanceBuyer = await acceptedTokens.map(async (token) => {
const mint = hexToPublicKey(token.address);
return getSplBalance(connection, mint, buyer.publicKey);
});
const startingBalanceContributor = await acceptedTokens.map(async (token) => {
const mint = hexToPublicKey(token.address);
return getSplBalance(connection, mint, program.programId);
});
// contribute twice
// now go about your business
// contribute multiple times
const saleId = dummyConductor.getSaleId();
for (const amount of contributionAmounts) {
const tx = await contributor.contribute(owner, saleId, contributionTokenIndex, new BN(amount));
for (const [tokenIndex, contributionAmounts] of contributions) {
for (const amount of contributionAmounts) {
const tx = await contributor.contribute(orchestrator, saleId, tokenIndex, new BN(amount));
}
}
// check buyer state
{
const saleId = dummyConductor.getSaleId();
const buyerState = await contributor.getBuyer(saleId, owner.publicKey);
expect(buyerState.status).has.key("active");
const expectedContributedValues = [
totalContributionAmount,
new BN(0),
new BN(0),
new BN(0),
new BN(0),
totalContributions[0],
new BN(0),
new BN(0),
totalContributions[1],
new BN(0),
new BN(0),
new BN(0),
new BN(0),
];
const contributed = buyerState.contributed;
expect(contributed.length).to.equal(10);
for (let i = 0; i < 10; ++i) {
expect(contributed[i].toString()).to.equal(expectedContributedValues[i].toString());
// check buyer state
{
const buyerState = await contributor.getBuyer(saleId, orchestrator.publicKey);
expect(buyerState.status).has.key("active");
const contributed = buyerState.contributed;
for (let i = 0; i < expectedContributedValues.length; ++i) {
expect(contributed[i].toString()).to.equal(expectedContributedValues[i].toString());
}
}
// check sale state
{
const saleState = await contributor.getSale(saleId);
// check totals
const totals: any = saleState.totals;
for (let i = 0; i < expectedContributedValues.length; ++i) {
const total = totals[i];
expect(total.contributions.toString()).to.equal(expectedContributedValues[i].toString());
expect(total.allocations.toString()).to.equal("0");
expect(total.excessContributions.toString()).to.equal("0");
}
}
}
@ -207,7 +267,7 @@ describe("anchor-contributor", () => {
let caughtError = false;
try {
const tx = await contributor.contribute(owner, saleId, tokenIndex, amount);
const tx = await contributor.contribute(orchestrator, saleId, tokenIndex, amount);
} catch (e) {
caughtError = e.error.errorCode.code == "InvalidTokenIndex";
}
@ -247,7 +307,7 @@ describe("anchor-contributor", () => {
let caughtError = false;
try {
const tx = await contributor.contribute(owner, saleId, tokenIndex, amount);
const tx = await contributor.contribute(orchestrator, saleId, tokenIndex, amount);
} catch (e) {
caughtError = e.error.errorCode.code == "SaleEnded";
}
@ -255,18 +315,12 @@ describe("anchor-contributor", () => {
if (!caughtError) {
throw Error("did not catch expected error");
}
// check buyer state
{
const saleId = dummyConductor.getSaleId();
//const buyerState = await contributor.getBuyer(saleId, owner.publicKey);
}
// TODO: check balances on contract and buyer
});
// TODO
it("Orchestrator Seals Sale with Signed VAA", async () => {
expect(false).to.be.true;
//const saleSealedVaa = dummyConductor.sealSale();
//const tx = await contributor.sealSale(orchestrator, saleSealedVaa);
});
// TODO
@ -292,24 +346,32 @@ describe("anchor-contributor", () => {
describe("Conduct Aborted Sale", () => {
// contributor info
const contributionTokenIndex = 2;
const contributionAmounts = ["2000000000", "3000000000"];
const totalContributionAmount = contributionAmounts.map((x) => new BN(x)).reduce((prev, curr) => prev.add(curr));
const contributions = new Map<number, string[]>();
contributions.set(2, ["8700000000", "6500000000"]);
contributions.set(8, ["4300000000", "2100000000"]);
const totalContributions: BN[] = [];
contributions.forEach((amounts) => {
totalContributions.push(amounts.map((x) => new BN(x)).reduce((prev, curr) => prev.add(curr)));
});
// squirrel away associated sale token account
let saleTokenAccount: AssociatedTokenAccount;
it("Create ATA for Sale Token if Non-Existent", async () => {
saleTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
orchestrator,
dummyConductor.getSaleTokenOnSolana(),
program.programId
);
});
it("Orchestrator Initialize Sale with Signed VAA", async () => {
// set up saleInit vaa
const tokenAccountKey = tryHexToNativeString(
"00000000000000000000000083752ecafebf4707258dedffbd9c7443148169db",
CHAIN_ID_SOLANA
); // placeholder
const startTime = 10 + (await getBlockTime(connection));
const duration = 5; // seconds
const initSaleVaa = dummyConductor.createSale(
await getBlockTime(connection),
duration,
new web3.PublicKey(tokenAccountKey)
);
const tx = await contributor.initSale(owner, initSaleVaa);
const initSaleVaa = dummyConductor.createSale(startTime, duration, saleTokenAccount.address);
const tx = await contributor.initSale(orchestrator, initSaleVaa);
{
const saleId = dummyConductor.getSaleId();
@ -352,16 +414,19 @@ describe("anchor-contributor", () => {
}
// now go about your business
// contribute multiple times
const saleId = dummyConductor.getSaleId();
for (const amount of contributionAmounts) {
const tx = await contributor.contribute(owner, saleId, contributionTokenIndex, new BN(amount));
for (const [tokenIndex, contributionAmounts] of contributions) {
for (const amount of contributionAmounts) {
const tx = await contributor.contribute(orchestrator, saleId, tokenIndex, new BN(amount));
}
}
});
it("Orchestrator Aborts Sale with Signed VAA", async () => {
// TODO: need to abort sale
const saleAbortedVaa = dummyConductor.abortSale(await getBlockTime(connection));
const tx = await contributor.abortSale(owner, saleAbortedVaa);
const tx = await contributor.abortSale(orchestrator, saleAbortedVaa);
{
const saleId = dummyConductor.getSaleId();
@ -376,7 +441,7 @@ describe("anchor-contributor", () => {
let caughtError = false;
try {
const tx = await contributor.abortSale(owner, saleAbortedVaa);
const tx = await contributor.abortSale(orchestrator, saleAbortedVaa);
} catch (e) {
caughtError = e.error.errorCode.code == "SaleEnded";
}
@ -401,23 +466,4 @@ describe("anchor-contributor", () => {
expect(false).to.be.true;
});
});
/*
it("creates custody accounts for given token", async () => {
setDefaultWasm("node");
const { parse_vaa } = await importCoreWasm();
const parsedVaa = parse_vaa(initSaleVaa);
const parsedPayload = await parseSaleInit(parsedVaa.payload);
console.log(parsedPayload);
//Iterate through all accepted tokens on Solana
let solanaTokenAddresses = [];
for(let addr of solanaTokenAddresses) {
await createAssociatedTokenAccount(connection, owner, addr, program.programId);
}
});
*/
});

View File

@ -1,11 +1,14 @@
import { web3 } from "@project-serum/anchor";
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, tryNativeToHexString } from "@certusone/wormhole-sdk";
import { createMint, getOrCreateAssociatedTokenAccount, mintTo } from "@solana/spl-token";
import { BigNumber } from "ethers";
import { toBigNumberHex } from "./utils";
import { CONDUCTOR_ADDRESS, CONDUCTOR_CHAIN } from "./consts";
import { signAndEncodeVaa } from "./wormhole";
import { web3 } from "@project-serum/anchor";
// sale struct info
export const MAX_ACCEPTED_TOKENS = 8;
const NUM_BYTES_ACCEPTED_TOKEN = 33;
const NUM_BYTES_ALLOCATION = 65;
@ -18,6 +21,9 @@ export class DummyConductor {
initSaleVaa: Buffer;
saleTokenOnSolana: string;
acceptedTokens: AcceptedToken[];
constructor() {
this.saleId = 0;
this.wormholeSequence = 0;
@ -26,30 +32,50 @@ export class DummyConductor {
this.saleEnd = 0;
this.acceptedTokens = [];
this.acceptedTokens.push(makeAcceptedToken(2, "So11111111111111111111111111111111111111112"));
}
async attestSaleToken(connection: web3.Connection, payer: web3.Keypair): Promise<void> {
const mint = await createMint(connection, payer, payer.publicKey, payer.publicKey, 9);
this.saleTokenOnSolana = mint.toBase58();
return;
}
getSaleTokenOnSolana(): web3.PublicKey {
return new web3.PublicKey(this.saleTokenOnSolana);
}
async createAcceptedTokens(connection: web3.Connection, payer: web3.Keypair): Promise<AcceptedToken[]> {
const tokenIndices = [2, 3, 5, 8, 13, 21, 34, 55];
for (let i = 0; i < MAX_ACCEPTED_TOKENS; ++i) {
// just make everything the same number of decimals (9)
const mint = await createMint(connection, payer, payer.publicKey, payer.publicKey, 9);
const acceptedToken = makeAcceptedToken(tokenIndices[i], mint.toBase58());
this.acceptedTokens.push(acceptedToken);
}
return this.acceptedTokens;
}
getSaleId(): Buffer {
return Buffer.from(toBigNumberHex(this.saleId, 32), "hex");
}
createSale(blockTime: number, duration: number, associatedTokenAddress: web3.PublicKey): Buffer {
createSale(startTime: number, duration: number, associatedSaleTokenAddress: web3.PublicKey): Buffer {
// uptick saleId for every new sale
++this.saleId;
// set up sale time based on block time
this.saleStart = blockTime + 5;
this.saleStart = startTime;
this.saleEnd = this.saleStart + duration;
this.initSaleVaa = signAndEncodeVaa(
blockTime,
startTime,
this.nonce,
CONDUCTOR_CHAIN,
Buffer.from(CONDUCTOR_ADDRESS).toString("hex"),
this.wormholeSequence,
encodeSaleInit(
this.saleId,
tryNativeToHexString(associatedTokenAddress.toString(), CHAIN_ID_SOLANA),
tryNativeToHexString(associatedSaleTokenAddress.toString(), CHAIN_ID_SOLANA),
this.tokenChain,
this.tokenDecimals,
this.saleStart,
@ -76,7 +102,6 @@ export class DummyConductor {
//associatedTokenAddress = "00000000000000000000000083752ecafebf4707258dedffbd9c7443148169db";
tokenChain = CHAIN_ID_ETH as number;
tokenDecimals = 18;
acceptedTokens: AcceptedToken[];
recipient = tryNativeToHexString("0x22d491bde2303f2f43325b2108d26f1eaba1e32b", CHAIN_ID_ETH);
// wormhole nonce

View File

@ -1,4 +1,6 @@
import { web3 } from "@project-serum/anchor";
import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
import { tryHexToNativeString, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { BigNumber, BigNumberish } from "ethers";
export function toBigNumberHex(value: BigNumberish, numBytes: number): string {
@ -16,3 +18,13 @@ export async function getBlockTime(connection: web3.Connection): Promise<number>
const slot = await connection.getSlot();
return connection.getBlockTime(slot);
}
export async function getSplBalance(connection: web3.Connection, mint: web3.PublicKey, owner: web3.PublicKey) {
const tokenAccount = await getAssociatedTokenAddress(mint, owner);
const account = await getAccount(connection, tokenAccount);
return account.amount;
}
export function hexToPublicKey(hexlified: string): web3.PublicKey {
return new web3.PublicKey(tryHexToNativeString(hexlified, CHAIN_ID_SOLANA));
}

View File

@ -0,0 +1 @@
[156,58,190,217,217,182,164,165,16,94,2,148,52,60,124,179,124,246,51,210,65,41,197,4,69,101,18,51,144,149,178,85,170,87,2,76,226,26,32,176,106,152,191,126,156,67,26,119,102,204,192,42,216,85,108,243,227,56,255,208,45,59,124,205]

View File

@ -2,9 +2,9 @@
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"lib": ["es2020"],
"module": "commonjs",
"target": "es6",
"target": "es2020",
"esModuleInterop": true,
"moduleResolution": "node"
}