solana.js: oracle stake ixns

This commit is contained in:
Conner Gallagher 2022-12-01 13:23:35 -07:00
parent 37af41d11e
commit 5a49b6269c
15 changed files with 568 additions and 237 deletions

View File

@ -698,7 +698,7 @@ export class AggregatorAccount extends Account<types.AggregatorAccountData> {
public async openRoundInstruction(
payer: PublicKey,
params: Partial<{ payoutWallet: PublicKey }>
params?: { payoutWallet?: PublicKey }
): Promise<TransactionObject> {
const aggregatorData = await this.loadData();
const queueAccount = new QueueAccount(
@ -760,9 +760,9 @@ export class AggregatorAccount extends Account<types.AggregatorAccountData> {
return new TransactionObject(payer, ixns, []);
}
public async openRound(
params: Partial<{ payoutWallet: PublicKey }>
): Promise<TransactionSignature> {
public async openRound(params?: {
payoutWallet?: PublicKey;
}): Promise<TransactionSignature> {
const openRoundTxn = await this.openRoundInstruction(
this.program.walletPubkey,
params

View File

@ -192,7 +192,7 @@ export class BufferRelayerAccount extends Account<types.BufferRelayerAccountData
);
const tokenAmountBN = new BN(tokenAccount.amount.toString());
if (tokenAmountBN.lt(queue.reward)) {
const wrapTxn = await this.program.mint.wrapInstruction(payer, {
const wrapTxn = await this.program.mint.wrapInstructions(payer, {
fundUpTo: new Big(this.program.mint.fromTokenAmountBN(queue.reward)),
});
ixns.push(...wrapTxn.ixns);

View File

@ -120,7 +120,7 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
(await program.mint.getBalance(funderAuthority)) ?? 0;
if (loadAmount && funderTokenBalance < loadAmount) {
const wrapIxns = await program.mint.wrapInstruction(
const wrapIxns = await program.mint.wrapInstructions(
payer,
{ amount: loadAmount },
params.funderAuthority
@ -354,7 +354,7 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
const funderBalance =
(await this.program.mint.getBalance(funderAuthority)) ?? 0;
if (funderBalance < params.loadAmount) {
const wrapIxns = await this.program.mint.unwrapInstruction(
const wrapIxns = await this.program.mint.unwrapInstructions(
payer,
params.loadAmount,
params.funderAuthority
@ -395,20 +395,6 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
return new TransactionObject(payer, ixns, signers);
}
public async withdraw(params: {
amount: number;
unwrap?: boolean;
withdrawWallet?: PublicKey;
withdrawAuthority?: Keypair;
}): Promise<TransactionSignature> {
const withdrawTxn = await this.withdrawInstruction(
this.program.walletPubkey,
params
);
const txnSignature = await this.program.signAndSend(withdrawTxn);
return txnSignature;
}
public async withdrawInstruction(
payer: PublicKey,
params: {
@ -499,7 +485,7 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
if (params.unwrap) {
txns.push(
await this.program.mint.unwrapInstruction(
await this.program.mint.unwrapInstructions(
payer,
params.amount,
params.withdrawAuthority
@ -515,6 +501,20 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
return packed[0];
}
public async withdraw(params: {
amount: number;
unwrap?: boolean;
withdrawWallet?: PublicKey;
withdrawAuthority?: Keypair;
}): Promise<TransactionSignature> {
const withdrawTxn = await this.withdrawInstruction(
this.program.walletPubkey,
params
);
const txnSignature = await this.program.signAndSend(withdrawTxn);
return txnSignature;
}
public async setAuthority(params: {
newAuthority: PublicKey;
withdrawAuthority: Keypair;

View File

@ -4,6 +4,7 @@ import { Account, OnAccountChangeCallback } from './account';
import * as anchor from '@project-serum/anchor';
import { SwitchboardProgram } from '../program';
import {
Commitment,
Keypair,
PublicKey,
SystemProgram,
@ -30,6 +31,35 @@ export class OracleAccount extends Account<types.OracleAccountData> {
*/
public size = this.program.account.oracleAccountData.size;
decode(data: Buffer): types.OracleAccountData {
try {
return types.OracleAccountData.decode(data);
} catch {
return this.program.coder.decode<types.OracleAccountData>(
OracleAccount.accountName,
data
);
}
}
/**
* Invoke a callback each time an OracleAccount's data has changed on-chain.
* @param callback - the callback invoked when the oracle state changes
* @param commitment - optional, the desired transaction finality. defaults to 'confirmed'
* @returns the websocket subscription id
*/
onChange(
callback: OnAccountChangeCallback<types.OracleAccountData>,
commitment: Commitment = 'confirmed'
): number {
return this.program.connection.onAccountChange(
this.publicKey,
accountInfo => {
callback(this.decode(accountInfo.data));
}
);
}
/**
* Retrieve and decode the {@linkcode types.OracleAccountData} stored in this account.
*/
@ -42,98 +72,6 @@ export class OracleAccount extends Account<types.OracleAccountData> {
return data;
}
public static async createInstructions(
program: SwitchboardProgram,
payer: PublicKey,
params: {
queueAccount: QueueAccount;
} & OracleInitParams
): Promise<[OracleAccount, TransactionObject]> {
const tokenWallet = Keypair.generate();
// console.log(`tokenWallet`, tokenWallet.publicKey.toBase58());
const authority = params.authority?.publicKey ?? payer;
const [oracleAccount, oracleBump] = OracleAccount.fromSeed(
program,
params.queueAccount.publicKey,
tokenWallet.publicKey
);
const ixns = [
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: tokenWallet.publicKey,
space: spl.ACCOUNT_SIZE,
lamports: await program.connection.getMinimumBalanceForRentExemption(
spl.ACCOUNT_SIZE
),
programId: spl.TOKEN_PROGRAM_ID,
}),
spl.createInitializeAccountInstruction(
tokenWallet.publicKey,
program.mint.address,
authority
),
spl.createSetAuthorityInstruction(
tokenWallet.publicKey,
authority,
spl.AuthorityType.AccountOwner,
program.programState.publicKey
),
types.oracleInit(
program,
{
params: {
name: new Uint8Array(
Buffer.from(params.name ?? '', 'utf8').slice(0, 32)
),
metadata: new Uint8Array(
Buffer.from(params.metadata ?? '', 'utf8').slice(0, 128)
),
oracleBump,
stateBump: program.programState.bump,
},
},
{
oracle: oracleAccount.publicKey,
oracleAuthority: authority,
wallet: tokenWallet.publicKey,
programState: program.programState.publicKey,
queue: params.queueAccount.publicKey,
payer,
systemProgram: SystemProgram.programId,
}
),
];
return [
new OracleAccount(program, oracleAccount.publicKey),
new TransactionObject(
payer,
ixns,
params.authority ? [tokenWallet, params.authority] : [tokenWallet]
),
];
}
public static async create(
program: SwitchboardProgram,
params: {
queueAccount: QueueAccount;
} & OracleInitParams
): Promise<[OracleAccount, TransactionSignature]> {
const [oracleAccount, txnObject] = await OracleAccount.createInstructions(
program,
program.walletPubkey,
params
);
const txnSignature = await program.signAndSend(txnObject);
return [oracleAccount, txnSignature];
}
/**
* Loads an OracleAccount from the expected PDA seed format.
* @param program The Switchboard program for the current connection.
@ -153,24 +91,172 @@ export class OracleAccount extends Account<types.OracleAccountData> {
return [new OracleAccount(program, publicKey), bump];
}
decode(data: Buffer): types.OracleAccountData {
try {
return types.OracleAccountData.decode(data);
} catch {
return this.program.coder.decode<types.OracleAccountData>(
OracleAccount.accountName,
data
);
public static async createInstructions(
program: SwitchboardProgram,
payer: PublicKey,
params: {
queueAccount: QueueAccount;
} & OracleInitParams &
OracleStakeParams
): Promise<[OracleAccount, TransactionObject]> {
const tokenWallet = Keypair.generate();
const authority = params.authority?.publicKey ?? payer;
const txns: TransactionObject[] = [];
const [oracleAccount, oracleBump] = OracleAccount.fromSeed(
program,
params.queueAccount.publicKey,
tokenWallet.publicKey
);
const oracleInit = new TransactionObject(
payer,
[
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: tokenWallet.publicKey,
space: spl.ACCOUNT_SIZE,
lamports: await program.connection.getMinimumBalanceForRentExemption(
spl.ACCOUNT_SIZE
),
programId: spl.TOKEN_PROGRAM_ID,
}),
spl.createInitializeAccountInstruction(
tokenWallet.publicKey,
program.mint.address,
authority
),
spl.createSetAuthorityInstruction(
tokenWallet.publicKey,
authority,
spl.AuthorityType.AccountOwner,
program.programState.publicKey
),
types.oracleInit(
program,
{
params: {
name: new Uint8Array(
Buffer.from(params.name ?? '', 'utf8').slice(0, 32)
),
metadata: new Uint8Array(
Buffer.from(params.metadata ?? '', 'utf8').slice(0, 128)
),
oracleBump,
stateBump: program.programState.bump,
},
},
{
oracle: oracleAccount.publicKey,
oracleAuthority: authority,
wallet: tokenWallet.publicKey,
programState: program.programState.publicKey,
queue: params.queueAccount.publicKey,
payer,
systemProgram: SystemProgram.programId,
}
),
],
params.authority ? [params.authority, tokenWallet] : [tokenWallet]
);
txns.push(oracleInit);
if (params.stakeAmount && params.stakeAmount > 0) {
const depositTxn = await oracleAccount.stakeInstructions(payer, {
stakeAmount: params.stakeAmount,
funderAuthority: params.funderAuthority,
funderTokenAccount: params.funderTokenAccount,
tokenAccount: tokenWallet.publicKey,
});
txns.push(depositTxn);
}
const packed = TransactionObject.pack(txns);
if (packed.length > 1) {
throw new Error(`Expected a single TransactionObject`);
}
return [oracleAccount, packed[0]];
}
onChange(callback: OnAccountChangeCallback<types.OracleAccountData>): number {
return this.program.connection.onAccountChange(
this.publicKey,
accountInfo => {
callback(this.decode(accountInfo.data));
}
public static async create(
program: SwitchboardProgram,
params: {
queueAccount: QueueAccount;
} & OracleInitParams &
OracleStakeParams
): Promise<[OracleAccount, TransactionSignature]> {
const [oracleAccount, txnObject] = await OracleAccount.createInstructions(
program,
program.walletPubkey,
params
);
const txnSignature = await program.signAndSend(txnObject);
return [oracleAccount, txnSignature];
}
async stakeInstructions(
payer: PublicKey,
params: OracleStakeParams & { tokenAccount?: PublicKey }
): Promise<TransactionObject> {
if (!params.stakeAmount || params.stakeAmount <= 0) {
throw new Error(`stake amount should be greater than 0`);
}
const tokenWallet =
params.tokenAccount ?? (await this.loadData()).tokenAccount;
const funderAuthority = params.funderAuthority?.publicKey ?? payer;
const funderTokenAccount =
this.program.mint.getAssociatedAddress(funderAuthority);
const funderTokenAccountInfo = await this.program.connection.getAccountInfo(
funderTokenAccount
);
let wrapFundsTxn: TransactionObject;
if (!funderTokenAccountInfo) {
let userTokenAccount: PublicKey;
[userTokenAccount, wrapFundsTxn] =
await this.program.mint.createWrappedUserInstructions(
payer,
params.stakeAmount,
params.funderAuthority
);
} else {
wrapFundsTxn = await this.program.mint.wrapInstructions(
payer,
{ amount: params.stakeAmount },
params.funderAuthority
);
}
wrapFundsTxn.add(
spl.createTransferInstruction(
funderTokenAccount,
tokenWallet,
funderAuthority,
this.program.mint.toTokenAmount(params.stakeAmount)
)
);
return wrapFundsTxn;
}
async stake(
params: OracleStakeParams & { tokenAccount?: PublicKey }
): Promise<TransactionSignature> {
const stakeTxn = await this.stakeInstructions(
this.program.walletPubkey,
params
);
const txnSignature = await this.program.signAndSend(stakeTxn);
return txnSignature;
}
heartbeatInstruction(
@ -201,7 +287,7 @@ export class OracleAccount extends Account<types.OracleAccountData> {
);
}
async heartbeat(params: {
async heartbeat(params?: {
queueAccount: QueueAccount;
tokenWallet?: PublicKey;
queueAuthority?: PublicKey;
@ -209,23 +295,27 @@ export class OracleAccount extends Account<types.OracleAccountData> {
permission?: [PermissionAccount, number];
authority?: Keypair;
}): Promise<TransactionSignature> {
const tokenWallet =
params.tokenWallet ?? (await this.loadData()).tokenAccount;
const oracle = await this.loadData();
const tokenWallet = params?.tokenWallet ?? oracle.tokenAccount;
const queue = params.queue ?? (await params.queueAccount.loadData());
const oracles = await params.queueAccount.loadOracles();
const queueAccount =
params?.queueAccount ??
new QueueAccount(this.program, oracle.queuePubkey);
const queue = params?.queue ?? (await queueAccount.loadData());
const oracles = await queueAccount.loadOracles();
let lastPubkey = this.publicKey;
if (queue.size !== 0) {
if (oracles.length !== 0) {
lastPubkey = oracles[queue.gcIdx];
}
const [permissionAccount, permissionBump] =
params.permission ??
params?.permission ??
PermissionAccount.fromSeed(
this.program,
params.queueAuthority ?? queue.authority,
params.queueAccount.publicKey,
queue.authority,
queueAccount.publicKey,
this.publicKey
);
try {
@ -236,19 +326,29 @@ export class OracleAccount extends Account<types.OracleAccountData> {
);
}
if (
params?.authority &&
!oracle.oracleAuthority.equals(params.authority.publicKey)
) {
throw new errors.IncorrectAuthority(
oracle.oracleAuthority,
params.authority.publicKey
);
}
const heartbeatTxn = new TransactionObject(
this.program.walletPubkey,
[
this.heartbeatInstruction(this.program.walletPubkey, {
tokenWallet: tokenWallet,
gcOracle: lastPubkey,
oracleQueue: params.queueAccount.publicKey,
oracleQueue: queueAccount.publicKey,
dataBuffer: queue.dataBuffer,
permission: [permissionAccount, permissionBump],
authority: params.authority ? params.authority.publicKey : undefined,
authority: oracle.oracleAuthority,
}),
],
params.authority ? [params.authority] : []
params?.authority ? [params.authority] : []
);
const txnSignature = await this.program.signAndSend(heartbeatTxn);
@ -357,3 +457,12 @@ export interface OracleInitParams {
/** Alternative keypair that will be the authority for the oracle. If not set the payer will be used. */
authority?: Keypair;
}
export interface OracleStakeParams {
/** The amount of funds to deposit into the oracle's staking wallet. The oracle must have the {@linkcode QueueAccount} minStake before being permitted to heartbeat and join the queue. */
stakeAmount?: number;
/** The tokenAccount for the account funding the staking wallet. Will default to the payer's associatedTokenAccount if not provided. */
funderTokenAccount?: PublicKey;
/** The funderTokenAccount authority for approving the transfer of funds from the funderTokenAccount into the oracle staking wallet. Will default to the payer if not provided. */
funderAuthority?: Keypair;
}

View File

@ -7,6 +7,11 @@ import {
} from '@solana/web3.js';
import * as errors from '../errors';
import * as types from '../generated';
import {
PermitOracleHeartbeat,
PermitOracleQueueUsage,
PermitVrfRequests,
} from '../generated/types/SwitchboardPermission';
import { SwitchboardProgram } from '../program';
import { TransactionObject } from '../transaction';
import { Account } from './account';
@ -20,6 +25,29 @@ export interface PermissionAccountInitParams {
authority: PublicKey;
}
export interface PermitNoneJSON {
kind: 'PermitNone';
}
export class PermitNone {
static readonly discriminator = 0;
static readonly kind = 'NONE';
readonly discriminator = 0;
readonly kind = 'PermitNone';
toJSON(): PermitNoneJSON {
return {
kind: 'PermitNone',
};
}
toEncodable() {
return {
PermitOracleHeartbeat: {},
};
}
}
export interface PermissionSetParams {
/** The {@linkcode types.SwitchboardPermission} to set for the grantee. */
permission: types.SwitchboardPermissionKind;
@ -39,6 +67,25 @@ export interface PermissionSetParams {
export class PermissionAccount extends Account<types.PermissionAccountData> {
static accountName = 'PermissionAccountData';
static getPermissions(
permission: types.PermissionAccountData
): types.SwitchboardPermissionKind | PermitNone {
switch (permission.permissions) {
case 0:
return new PermitNone();
case 1:
return new PermitOracleHeartbeat();
case 2:
return new PermitOracleQueueUsage();
case 3:
return new PermitVrfRequests();
}
throw new Error(
`Failed to find the assigned permissions, expected a value from 0 - 3, received ${permission.permissions}`
);
}
/**
* Loads a PermissionAccount from the expected PDA seed format.
* @param program The Switchboard program for the current connection.

View File

@ -24,7 +24,11 @@ import { BufferRelayerAccount, BufferRelayerInit } from './bufferRelayAccount';
import { CrankAccount, CrankInitParams } from './crankAccount';
import { JobAccount, JobInitParams } from './jobAccount';
import { LeaseAccount } from './leaseAccount';
import { OracleAccount, OracleInitParams } from './oracleAccount';
import {
OracleAccount,
OracleInitParams,
OracleStakeParams,
} from './oracleAccount';
import { PermissionAccount, PermissionSetParams } from './permissionAccount';
import { QueueDataBuffer } from './queueDataBuffer';
import { VrfAccount, VrfInitParams } from './vrfAccount';
@ -274,8 +278,10 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
public async createOracleInstructions(
/** The publicKey of the account that will pay for the new accounts. Will also be used as the account authority if no other authority is provided. */
payer: PublicKey,
params: OracleInitParams & Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[OracleAccount, TransactionObject]> {
params: OracleInitParams &
OracleStakeParams &
Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[OracleAccount, Array<TransactionObject>]> {
const queue = await this.loadData();
const [oracleAccount, createOracleTxnObject] =
@ -302,7 +308,10 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
return [
oracleAccount,
createOracleTxnObject.combine(createPermissionTxnObject),
TransactionObject.pack([
createOracleTxnObject,
createPermissionTxnObject,
]),
];
}
@ -326,8 +335,10 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
* ```
*/
public async createOracle(
params: OracleInitParams & Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[OracleAccount, TransactionSignature]> {
params: OracleInitParams &
OracleStakeParams &
Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[OracleAccount, Array<TransactionSignature>]> {
const signers: Keypair[] = [];
const queue = await this.loadData();
@ -344,9 +355,9 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
params
);
const signature = await this.program.signAndSend(txn);
const signatures = await this.program.signAndSendAll(txn);
return [oracleAccount, signature];
return [oracleAccount, signatures];
}
/**

View File

@ -1,4 +1,5 @@
import * as anchor from '@project-serum/anchor';
import { PublicKey } from '@solana/web3.js';
export class SwitchboardProgramIsBrowserError extends Error {
constructor() {
@ -69,3 +70,11 @@ export class TransactionMissingSignerError extends Error {
Object.setPrototypeOf(this, TransactionMissingSignerError.prototype);
}
}
export class IncorrectAuthority extends Error {
constructor(expectedAuthority: PublicKey, receivedAuthority: PublicKey) {
super(
`incorrect authority, expected ${expectedAuthority}, received ${receivedAuthority}`
);
Object.setPrototypeOf(this, IncorrectAuthority.prototype);
}
}

View File

@ -31,8 +31,11 @@ export class Mint {
return this.provider.connection;
}
public static async load(provider: anchor.AnchorProvider): Promise<Mint> {
const splMint = await spl.getMint(provider.connection, Mint.native);
public static async load(
provider: anchor.AnchorProvider,
mint = Mint.native
): Promise<Mint> {
const splMint = await spl.getMint(provider.connection, mint);
return new Mint(provider, splMint);
}
@ -84,15 +87,11 @@ export class Mint {
return this.fromTokenAmount(userAccount.amount);
}
public getAssociatedAddress(
user: anchor.web3.PublicKey
): anchor.web3.PublicKey {
public getAssociatedAddress(user: PublicKey): PublicKey {
return Mint.getAssociatedAddress(user);
}
public static getAssociatedAddress(
user: anchor.web3.PublicKey
): anchor.web3.PublicKey {
public static getAssociatedAddress(user: PublicKey): PublicKey {
const [associatedToken] = anchor.utils.publicKey.findProgramAddressSync(
[
user.toBuffer(),
@ -107,7 +106,7 @@ export class Mint {
public async getOrCreateAssociatedUser(
payer: PublicKey,
user?: Keypair
): Promise<anchor.web3.PublicKey> {
): Promise<PublicKey> {
const owner = user ? user.publicKey : payer;
const associatedToken = Mint.getAssociatedAddress(owner);
const accountInfo = await this.connection.getAccountInfo(associatedToken);
@ -120,9 +119,9 @@ export class Mint {
}
public async createAssocatedUser(
payer: anchor.web3.PublicKey,
payer: PublicKey,
user?: Keypair
): Promise<[anchor.web3.PublicKey, string]> {
): Promise<[PublicKey, string]> {
const [txn, associatedToken] = this.createAssocatedUserInstruction(
payer,
user
@ -133,7 +132,7 @@ export class Mint {
}
public static createAssocatedUserInstruction(
payer: anchor.web3.PublicKey,
payer: PublicKey,
user?: Keypair
): [TransactionObject, PublicKey] {
const owner = user ? user.publicKey : payer;
@ -184,8 +183,107 @@ export class Mint {
return [account, sig];
}
public async wrapInstruction(
payer: anchor.web3.PublicKey,
public async signAndSend(
txn: TransactionObject,
opts: anchor.web3.ConfirmOptions = {
skipPreflight: false,
maxRetries: 10,
}
): Promise<TransactionSignature> {
const blockhash = await this.connection.getLatestBlockhash();
const txnSignature = await this.provider.sendAndConfirm(
await this.provider.wallet.signTransaction(txn.toTxn(blockhash)),
txn.signers,
opts
);
return txnSignature;
}
}
export class NativeMint extends Mint {
public static async load(
provider: anchor.AnchorProvider
): Promise<NativeMint> {
const splMint = await spl.getMint(provider.connection, Mint.native);
return new NativeMint(provider, splMint);
}
public async createWrappedUserInstructions(
payer: PublicKey,
amount: number,
user?: Keypair
): Promise<[PublicKey, TransactionObject]> {
const owner = user ? user.publicKey : payer;
const associatedAddress = this.getAssociatedAddress(owner);
const associatedAccountInfo =
this.connection.getAccountInfo(associatedAddress);
if (!associatedAccountInfo) {
throw new Error(
`Associated token address already exists for this user ${owner}`
);
}
const ephemeralAccount = Keypair.generate();
const ephemeralWallet = this.getAssociatedAddress(
ephemeralAccount.publicKey
);
const wrapAmountLamports = this.toTokenAmount(amount);
return [
associatedAddress,
new TransactionObject(
payer,
[
spl.createAssociatedTokenAccountInstruction(
payer,
associatedAddress,
owner,
Mint.native
),
spl.createAssociatedTokenAccountInstruction(
payer,
ephemeralWallet,
ephemeralAccount.publicKey,
spl.NATIVE_MINT
),
SystemProgram.transfer({
fromPubkey: owner,
toPubkey: ephemeralWallet,
lamports: wrapAmountLamports,
}),
spl.createSyncNativeInstruction(ephemeralWallet),
spl.createTransferInstruction(
ephemeralWallet,
associatedAddress,
ephemeralAccount.publicKey,
wrapAmountLamports
),
spl.createCloseAccountInstruction(
ephemeralWallet,
owner,
ephemeralAccount.publicKey
),
],
user ? [user, ephemeralAccount] : [ephemeralAccount]
),
];
}
public async createWrappedUser(
payer: PublicKey,
amount: number,
user?: Keypair
): Promise<[PublicKey, TransactionSignature]> {
const [tokenAccount, createWrappedUserTxn] =
await this.createWrappedUserInstructions(payer, amount, user);
const txSignature = await this.signAndSend(createWrappedUserTxn);
return [tokenAccount, txSignature];
}
public async wrapInstructions(
payer: PublicKey,
params:
| {
amount: number;
@ -193,10 +291,6 @@ export class Mint {
| { fundUpTo: Big },
user?: Keypair
): Promise<TransactionObject> {
if (!this.address.equals(Mint.native)) {
throw new NativeMintOnlyError();
}
const ixns: TransactionInstruction[] = [];
const owner = user ? user.publicKey : payer;
@ -209,16 +303,6 @@ export class Mint {
userAccountInfo === null
? null
: spl.unpackAccount(userAddress, userAccountInfo);
// if (userAccount === null) {
// ixns.push(
// spl.createAssociatedTokenAccountInstruction(
// payer,
// userAddress,
// owner,
// Mint.native
// )
// );
// }
const tokenBalance = userAccount
? new Big(this.fromTokenAmount(userAccount.amount))
@ -283,7 +367,7 @@ export class Mint {
}
public async wrap(
payer: anchor.web3.PublicKey,
payer: PublicKey,
params:
| {
amount: number;
@ -291,25 +375,17 @@ export class Mint {
| { fundUpTo: Big },
user?: Keypair
) {
if (!this.address.equals(Mint.native)) {
throw new NativeMintOnlyError();
}
const wrapIxns = await this.wrapInstruction(payer, params, user);
const wrapIxns = await this.wrapInstructions(payer, params, user);
const txSignature = await this.signAndSend(wrapIxns);
return txSignature;
}
public async unwrapInstruction(
payer: anchor.web3.PublicKey,
public async unwrapInstructions(
payer: PublicKey,
amount?: number,
user?: Keypair
): Promise<TransactionObject> {
if (!this.address.equals(Mint.native)) {
throw new NativeMintOnlyError();
}
const owner = user ? user.publicKey : payer;
const ixns: TransactionInstruction[] = [];
@ -359,7 +435,7 @@ export class Mint {
}
public async unwrap(
payer: anchor.web3.PublicKey,
payer: PublicKey,
amount?: number,
user?: Keypair
): Promise<TransactionSignature> {
@ -367,24 +443,8 @@ export class Mint {
throw new NativeMintOnlyError();
}
const unwrapTxn = await this.unwrapInstruction(payer, amount, user);
const unwrapTxn = await this.unwrapInstructions(payer, amount, user);
const txSignature = await this.signAndSend(unwrapTxn);
return txSignature;
}
private async signAndSend(
txn: TransactionObject,
opts: anchor.web3.ConfirmOptions = {
skipPreflight: false,
maxRetries: 10,
}
): Promise<TransactionSignature> {
const blockhash = await this.connection.getLatestBlockhash();
const txnSignature = await this.provider.sendAndConfirm(
await this.provider.wallet.signTransaction(txn.toTxn(blockhash)),
txn.signers,
opts
);
return txnSignature;
}
}

View File

@ -9,9 +9,12 @@ import {
Transaction,
TransactionSignature,
} from '@solana/web3.js';
import { Mint } from './mint';
import { types } from './';
import { Mint, NativeMint } from './mint';
import { TransactionObject } from './transaction';
import { SwitchboardEvents } from './switchboardEvents';
import { fromCode as fromSwitchboardCode } from './generated/errors/custom';
import { fromCode as fromAnchorCode } from './generated/errors/anchor';
/**
* Switchboard Devnet Program ID
@ -86,7 +89,7 @@ export class SwitchboardProgram {
bump: number;
};
readonly mint: Mint;
readonly mint: NativeMint;
/**
* Constructor.
@ -94,7 +97,7 @@ export class SwitchboardProgram {
constructor(
program: anchor.Program,
cluster: Cluster | 'localnet',
mint: Mint
mint: NativeMint
) {
this._program = program;
this.cluster = cluster;
@ -169,7 +172,9 @@ export class SwitchboardProgram {
payerKeypair,
programId
);
const mint = await Mint.load(program.provider as anchor.AnchorProvider);
const mint = await NativeMint.load(
program.provider as anchor.AnchorProvider
);
return new SwitchboardProgram(program, cluster, mint);
};
@ -308,17 +313,37 @@ export class SwitchboardProgram {
s.publicKey.equals(txn.payer) || reqSigners.has(s.publicKey.toBase58())
);
const txnSignature = await this.provider.sendAndConfirm(
txn.toTxn(blockhash ?? (await this.connection.getLatestBlockhash())),
filteredSigners,
{
skipPreflight: false,
maxRetries: 10,
...opts,
}
const transaction = txn.toTxn(
blockhash ?? (await this.connection.getLatestBlockhash())
);
return txnSignature;
try {
const txnSignature = await this.provider.sendAndConfirm(
transaction,
filteredSigners,
{
skipPreflight: false,
maxRetries: 10,
...opts,
}
);
return txnSignature;
} catch (error) {
if ('code' in (error as any) && typeof (error as any).code === 'number') {
const switchboardError = fromSwitchboardCode((error as any).code);
if (switchboardError) {
throw switchboardError;
}
const anchorError = fromAnchorCode((error as any).code);
if (anchorError) {
throw anchorError;
}
}
throw error;
}
}
}

View File

@ -1,7 +1,5 @@
/* eslint-disable no-unused-vars */
import 'mocha';
import chai, { expect } from 'chai';
import assert from 'assert';
import * as sbv2 from '../src';
import { setupTest, TestContext } from './utilts';
@ -189,9 +187,8 @@ describe('Aggregator Tests', () => {
}
const aggregatorAccount = fundedAggregator;
const initialUserTokenBalance = await ctx.program.mint.getBalance(
ctx.payer.publicKey
);
const initialUserTokenBalance =
(await ctx.program.mint.getBalance(ctx.payer.publicKey)) ?? 0;
const [leaseAccount] = LeaseAccount.fromSeed(
ctx.program,
@ -214,6 +211,13 @@ describe('Aggregator Tests', () => {
);
}
const finalUserBalance = await ctx.program.mint.getBalance(
ctx.payer.publicKey
);
if (!finalUserBalance) {
throw new Error(`Users wrapped account was closed`);
}
const finalUserTokenBalance =
(await ctx.program.mint.getBalance(ctx.payer.publicKey)) ?? 0;
if (initialUserTokenBalance !== finalUserTokenBalance) {

View File

@ -1,5 +1,4 @@
import 'mocha';
import chai, { expect } from 'chai';
import assert from 'assert';
import { setupTest, TestContext } from './utilts';

View File

@ -1,6 +1,4 @@
import 'mocha';
import chai, { expect } from 'chai';
import assert from 'assert';
import * as anchor from '@project-serum/anchor';
import { setupTest, TestContext } from './utilts';

View File

@ -1,10 +1,18 @@
import 'mocha';
import assert from 'assert';
import * as sbv2 from '../src';
import { setupTest, TestContext } from './utilts';
import { Keypair } from '@solana/web3.js';
import { AggregatorAccount, OracleAccount, QueueAccount, types } from '../src';
import {
AggregatorAccount,
OracleAccount,
PermissionAccount,
QueueAccount,
types,
} from '../src';
import { OracleJob } from '@switchboard-xyz/common';
import { PermitOracleQueueUsage } from '../src/generated/types/SwitchboardPermission';
describe('Open Round Tests', () => {
let ctx: TestContext;
@ -15,17 +23,18 @@ describe('Open Round Tests', () => {
let queueAccount: QueueAccount;
let queue: types.OracleQueueAccountData;
let createOracleSignature1: string;
let createOracleSignature1: string[];
let oracleAccount1: OracleAccount;
let oracle1: types.OracleAccountData;
let createOracleSignature2: string;
let createOracleSignature2: string[];
let oracleAccount2: OracleAccount;
let oracle2: types.OracleAccountData;
let createAggregatorSignatures: string[];
let aggregatorAccount: AggregatorAccount;
let aggregator: types.AggregatorAccountData;
let aggregatorPermissionAccount: PermissionAccount;
before(async () => {
ctx = await setupTest();
@ -52,6 +61,7 @@ describe('Open Round Tests', () => {
name: 'oracle-1',
metadata: 'oracle-1',
queueAuthority,
enable: true,
});
oracle1 = await oracleAccount1.loadData();
@ -59,18 +69,18 @@ describe('Open Round Tests', () => {
name: 'oracle-2',
metadata: 'oracle-2',
queueAuthority,
enable: true,
});
oracle2 = await oracleAccount2.loadData();
[aggregatorAccount, createAggregatorSignatures] =
await queueAccount.createFeed({
queueAuthority: queueAuthority,
batchSize: 1,
minRequiredOracleResults: 1,
batchSize: 2,
minRequiredOracleResults: 2,
minRequiredJobResults: 1,
minUpdateDelaySeconds: 60,
minUpdateDelaySeconds: 5,
fundAmount: 1,
enable: true,
enable: false,
jobs: [
{
weight: 2,
@ -88,5 +98,64 @@ describe('Open Round Tests', () => {
},
],
});
[aggregatorPermissionAccount] = PermissionAccount.fromSeed(
ctx.program,
queueAuthority.publicKey,
queueAccount.publicKey,
aggregatorAccount.publicKey
);
});
it('fails to call open round when aggregator lacks permissions', async () => {
assert.rejects(
async () => {
await aggregatorAccount.openRound();
},
new RegExp(/custom program error: 0x1793/g)
// { code: 6035 } // PermissionDenied
);
});
it('sets aggregator permissions', async () => {
await aggregatorPermissionAccount.set({
permission: new PermitOracleQueueUsage(),
enable: true,
queueAuthority,
});
const permissions = await aggregatorPermissionAccount.loadData();
assert(
permissions.permissions === PermitOracleQueueUsage.discriminator + 1,
`Aggregator has incorrect permissions, expected ${
PermitOracleQueueUsage.kind
}, received ${PermissionAccount.getPermissions(permissions).kind}`
);
});
it('fails to call open round when not enough oracles are heartbeating', async () => {
assert.rejects(
async () => {
await aggregatorAccount.openRound();
},
new RegExp(/custom program error: 0x17a4/g)
// { code: 6052 } // InsufficientOracleQueueError
);
// still fails when queueSize < batchSize
await oracleAccount1.heartbeat();
assert.rejects(
async () => {
await aggregatorAccount.openRound();
},
new RegExp(/custom program error: 0x17a4/g)
// { code: 6052 } // InsufficientOracleQueueError
);
});
it('successfully calls open round', async () => {
await oracleAccount2.heartbeat();
// start heartbeating
await aggregatorAccount.openRound();
});
});

View File

@ -1,5 +1,4 @@
import 'mocha';
import chai, { expect } from 'chai';
import assert from 'assert';
import * as sbv2 from '../src';
@ -46,6 +45,7 @@ describe('Queue Tests', () => {
queueAuthority,
enable: true,
authority: oracleAuthority,
stakeAmount: 2,
});
const oracle = await oracleAccount.loadData();

View File

@ -58,8 +58,8 @@ export async function setupTest(): Promise<TestContext> {
payer.publicKey,
1 * LAMPORTS_PER_SOL
);
await program.connection.confirmTransaction(airdropTxn);
console.log(`Airdrop requested: ${airdropTxn}`);
await program.connection.confirmTransaction(airdropTxn);
}
// Check if programStateAccount exists