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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import { Account, OnAccountChangeCallback } from './account';
import * as anchor from '@project-serum/anchor'; import * as anchor from '@project-serum/anchor';
import { SwitchboardProgram } from '../program'; import { SwitchboardProgram } from '../program';
import { import {
Commitment,
Keypair, Keypair,
PublicKey, PublicKey,
SystemProgram, SystemProgram,
@ -30,6 +31,35 @@ export class OracleAccount extends Account<types.OracleAccountData> {
*/ */
public size = this.program.account.oracleAccountData.size; 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. * Retrieve and decode the {@linkcode types.OracleAccountData} stored in this account.
*/ */
@ -42,98 +72,6 @@ export class OracleAccount extends Account<types.OracleAccountData> {
return data; 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. * Loads an OracleAccount from the expected PDA seed format.
* @param program The Switchboard program for the current connection. * @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]; return [new OracleAccount(program, publicKey), bump];
} }
decode(data: Buffer): types.OracleAccountData { public static async createInstructions(
try { program: SwitchboardProgram,
return types.OracleAccountData.decode(data); payer: PublicKey,
} catch { params: {
return this.program.coder.decode<types.OracleAccountData>( queueAccount: QueueAccount;
OracleAccount.accountName, } & OracleInitParams &
data 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 { public static async create(
return this.program.connection.onAccountChange( program: SwitchboardProgram,
this.publicKey, params: {
accountInfo => { queueAccount: QueueAccount;
callback(this.decode(accountInfo.data)); } & 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( heartbeatInstruction(
@ -201,7 +287,7 @@ export class OracleAccount extends Account<types.OracleAccountData> {
); );
} }
async heartbeat(params: { async heartbeat(params?: {
queueAccount: QueueAccount; queueAccount: QueueAccount;
tokenWallet?: PublicKey; tokenWallet?: PublicKey;
queueAuthority?: PublicKey; queueAuthority?: PublicKey;
@ -209,23 +295,27 @@ export class OracleAccount extends Account<types.OracleAccountData> {
permission?: [PermissionAccount, number]; permission?: [PermissionAccount, number];
authority?: Keypair; authority?: Keypair;
}): Promise<TransactionSignature> { }): Promise<TransactionSignature> {
const tokenWallet = const oracle = await this.loadData();
params.tokenWallet ?? (await this.loadData()).tokenAccount; const tokenWallet = params?.tokenWallet ?? oracle.tokenAccount;
const queue = params.queue ?? (await params.queueAccount.loadData()); const queueAccount =
const oracles = await params.queueAccount.loadOracles(); params?.queueAccount ??
new QueueAccount(this.program, oracle.queuePubkey);
const queue = params?.queue ?? (await queueAccount.loadData());
const oracles = await queueAccount.loadOracles();
let lastPubkey = this.publicKey; let lastPubkey = this.publicKey;
if (queue.size !== 0) { if (oracles.length !== 0) {
lastPubkey = oracles[queue.gcIdx]; lastPubkey = oracles[queue.gcIdx];
} }
const [permissionAccount, permissionBump] = const [permissionAccount, permissionBump] =
params.permission ?? params?.permission ??
PermissionAccount.fromSeed( PermissionAccount.fromSeed(
this.program, this.program,
params.queueAuthority ?? queue.authority, queue.authority,
params.queueAccount.publicKey, queueAccount.publicKey,
this.publicKey this.publicKey
); );
try { 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( const heartbeatTxn = new TransactionObject(
this.program.walletPubkey, this.program.walletPubkey,
[ [
this.heartbeatInstruction(this.program.walletPubkey, { this.heartbeatInstruction(this.program.walletPubkey, {
tokenWallet: tokenWallet, tokenWallet: tokenWallet,
gcOracle: lastPubkey, gcOracle: lastPubkey,
oracleQueue: params.queueAccount.publicKey, oracleQueue: queueAccount.publicKey,
dataBuffer: queue.dataBuffer, dataBuffer: queue.dataBuffer,
permission: [permissionAccount, permissionBump], 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); 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. */ /** Alternative keypair that will be the authority for the oracle. If not set the payer will be used. */
authority?: Keypair; 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'; } from '@solana/web3.js';
import * as errors from '../errors'; import * as errors from '../errors';
import * as types from '../generated'; import * as types from '../generated';
import {
PermitOracleHeartbeat,
PermitOracleQueueUsage,
PermitVrfRequests,
} from '../generated/types/SwitchboardPermission';
import { SwitchboardProgram } from '../program'; import { SwitchboardProgram } from '../program';
import { TransactionObject } from '../transaction'; import { TransactionObject } from '../transaction';
import { Account } from './account'; import { Account } from './account';
@ -20,6 +25,29 @@ export interface PermissionAccountInitParams {
authority: PublicKey; 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 { export interface PermissionSetParams {
/** The {@linkcode types.SwitchboardPermission} to set for the grantee. */ /** The {@linkcode types.SwitchboardPermission} to set for the grantee. */
permission: types.SwitchboardPermissionKind; permission: types.SwitchboardPermissionKind;
@ -39,6 +67,25 @@ export interface PermissionSetParams {
export class PermissionAccount extends Account<types.PermissionAccountData> { export class PermissionAccount extends Account<types.PermissionAccountData> {
static accountName = '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. * Loads a PermissionAccount from the expected PDA seed format.
* @param program The Switchboard program for the current connection. * @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 { CrankAccount, CrankInitParams } from './crankAccount';
import { JobAccount, JobInitParams } from './jobAccount'; import { JobAccount, JobInitParams } from './jobAccount';
import { LeaseAccount } from './leaseAccount'; import { LeaseAccount } from './leaseAccount';
import { OracleAccount, OracleInitParams } from './oracleAccount'; import {
OracleAccount,
OracleInitParams,
OracleStakeParams,
} from './oracleAccount';
import { PermissionAccount, PermissionSetParams } from './permissionAccount'; import { PermissionAccount, PermissionSetParams } from './permissionAccount';
import { QueueDataBuffer } from './queueDataBuffer'; import { QueueDataBuffer } from './queueDataBuffer';
import { VrfAccount, VrfInitParams } from './vrfAccount'; import { VrfAccount, VrfInitParams } from './vrfAccount';
@ -274,8 +278,10 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
public async createOracleInstructions( 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. */ /** 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, payer: PublicKey,
params: OracleInitParams & Partial<Omit<PermissionSetParams, 'permission'>> params: OracleInitParams &
): Promise<[OracleAccount, TransactionObject]> { OracleStakeParams &
Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[OracleAccount, Array<TransactionObject>]> {
const queue = await this.loadData(); const queue = await this.loadData();
const [oracleAccount, createOracleTxnObject] = const [oracleAccount, createOracleTxnObject] =
@ -302,7 +308,10 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
return [ return [
oracleAccount, oracleAccount,
createOracleTxnObject.combine(createPermissionTxnObject), TransactionObject.pack([
createOracleTxnObject,
createPermissionTxnObject,
]),
]; ];
} }
@ -326,8 +335,10 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
* ``` * ```
*/ */
public async createOracle( public async createOracle(
params: OracleInitParams & Partial<Omit<PermissionSetParams, 'permission'>> params: OracleInitParams &
): Promise<[OracleAccount, TransactionSignature]> { OracleStakeParams &
Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[OracleAccount, Array<TransactionSignature>]> {
const signers: Keypair[] = []; const signers: Keypair[] = [];
const queue = await this.loadData(); const queue = await this.loadData();
@ -344,9 +355,9 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
params 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 * as anchor from '@project-serum/anchor';
import { PublicKey } from '@solana/web3.js';
export class SwitchboardProgramIsBrowserError extends Error { export class SwitchboardProgramIsBrowserError extends Error {
constructor() { constructor() {
@ -69,3 +70,11 @@ export class TransactionMissingSignerError extends Error {
Object.setPrototypeOf(this, TransactionMissingSignerError.prototype); 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; return this.provider.connection;
} }
public static async load(provider: anchor.AnchorProvider): Promise<Mint> { public static async load(
const splMint = await spl.getMint(provider.connection, Mint.native); provider: anchor.AnchorProvider,
mint = Mint.native
): Promise<Mint> {
const splMint = await spl.getMint(provider.connection, mint);
return new Mint(provider, splMint); return new Mint(provider, splMint);
} }
@ -84,15 +87,11 @@ export class Mint {
return this.fromTokenAmount(userAccount.amount); return this.fromTokenAmount(userAccount.amount);
} }
public getAssociatedAddress( public getAssociatedAddress(user: PublicKey): PublicKey {
user: anchor.web3.PublicKey
): anchor.web3.PublicKey {
return Mint.getAssociatedAddress(user); return Mint.getAssociatedAddress(user);
} }
public static getAssociatedAddress( public static getAssociatedAddress(user: PublicKey): PublicKey {
user: anchor.web3.PublicKey
): anchor.web3.PublicKey {
const [associatedToken] = anchor.utils.publicKey.findProgramAddressSync( const [associatedToken] = anchor.utils.publicKey.findProgramAddressSync(
[ [
user.toBuffer(), user.toBuffer(),
@ -107,7 +106,7 @@ export class Mint {
public async getOrCreateAssociatedUser( public async getOrCreateAssociatedUser(
payer: PublicKey, payer: PublicKey,
user?: Keypair user?: Keypair
): Promise<anchor.web3.PublicKey> { ): Promise<PublicKey> {
const owner = user ? user.publicKey : payer; const owner = user ? user.publicKey : payer;
const associatedToken = Mint.getAssociatedAddress(owner); const associatedToken = Mint.getAssociatedAddress(owner);
const accountInfo = await this.connection.getAccountInfo(associatedToken); const accountInfo = await this.connection.getAccountInfo(associatedToken);
@ -120,9 +119,9 @@ export class Mint {
} }
public async createAssocatedUser( public async createAssocatedUser(
payer: anchor.web3.PublicKey, payer: PublicKey,
user?: Keypair user?: Keypair
): Promise<[anchor.web3.PublicKey, string]> { ): Promise<[PublicKey, string]> {
const [txn, associatedToken] = this.createAssocatedUserInstruction( const [txn, associatedToken] = this.createAssocatedUserInstruction(
payer, payer,
user user
@ -133,7 +132,7 @@ export class Mint {
} }
public static createAssocatedUserInstruction( public static createAssocatedUserInstruction(
payer: anchor.web3.PublicKey, payer: PublicKey,
user?: Keypair user?: Keypair
): [TransactionObject, PublicKey] { ): [TransactionObject, PublicKey] {
const owner = user ? user.publicKey : payer; const owner = user ? user.publicKey : payer;
@ -184,8 +183,107 @@ export class Mint {
return [account, sig]; return [account, sig];
} }
public async wrapInstruction( public async signAndSend(
payer: anchor.web3.PublicKey, 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: params:
| { | {
amount: number; amount: number;
@ -193,10 +291,6 @@ export class Mint {
| { fundUpTo: Big }, | { fundUpTo: Big },
user?: Keypair user?: Keypair
): Promise<TransactionObject> { ): Promise<TransactionObject> {
if (!this.address.equals(Mint.native)) {
throw new NativeMintOnlyError();
}
const ixns: TransactionInstruction[] = []; const ixns: TransactionInstruction[] = [];
const owner = user ? user.publicKey : payer; const owner = user ? user.publicKey : payer;
@ -209,16 +303,6 @@ export class Mint {
userAccountInfo === null userAccountInfo === null
? null ? null
: spl.unpackAccount(userAddress, userAccountInfo); : spl.unpackAccount(userAddress, userAccountInfo);
// if (userAccount === null) {
// ixns.push(
// spl.createAssociatedTokenAccountInstruction(
// payer,
// userAddress,
// owner,
// Mint.native
// )
// );
// }
const tokenBalance = userAccount const tokenBalance = userAccount
? new Big(this.fromTokenAmount(userAccount.amount)) ? new Big(this.fromTokenAmount(userAccount.amount))
@ -283,7 +367,7 @@ export class Mint {
} }
public async wrap( public async wrap(
payer: anchor.web3.PublicKey, payer: PublicKey,
params: params:
| { | {
amount: number; amount: number;
@ -291,25 +375,17 @@ export class Mint {
| { fundUpTo: Big }, | { fundUpTo: Big },
user?: Keypair user?: Keypair
) { ) {
if (!this.address.equals(Mint.native)) { const wrapIxns = await this.wrapInstructions(payer, params, user);
throw new NativeMintOnlyError();
}
const wrapIxns = await this.wrapInstruction(payer, params, user);
const txSignature = await this.signAndSend(wrapIxns); const txSignature = await this.signAndSend(wrapIxns);
return txSignature; return txSignature;
} }
public async unwrapInstruction( public async unwrapInstructions(
payer: anchor.web3.PublicKey, payer: PublicKey,
amount?: number, amount?: number,
user?: Keypair user?: Keypair
): Promise<TransactionObject> { ): Promise<TransactionObject> {
if (!this.address.equals(Mint.native)) {
throw new NativeMintOnlyError();
}
const owner = user ? user.publicKey : payer; const owner = user ? user.publicKey : payer;
const ixns: TransactionInstruction[] = []; const ixns: TransactionInstruction[] = [];
@ -359,7 +435,7 @@ export class Mint {
} }
public async unwrap( public async unwrap(
payer: anchor.web3.PublicKey, payer: PublicKey,
amount?: number, amount?: number,
user?: Keypair user?: Keypair
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
@ -367,24 +443,8 @@ export class Mint {
throw new NativeMintOnlyError(); 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); const txSignature = await this.signAndSend(unwrapTxn);
return txSignature; 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, Transaction,
TransactionSignature, TransactionSignature,
} from '@solana/web3.js'; } from '@solana/web3.js';
import { Mint } from './mint'; import { types } from './';
import { Mint, NativeMint } from './mint';
import { TransactionObject } from './transaction'; import { TransactionObject } from './transaction';
import { SwitchboardEvents } from './switchboardEvents'; import { SwitchboardEvents } from './switchboardEvents';
import { fromCode as fromSwitchboardCode } from './generated/errors/custom';
import { fromCode as fromAnchorCode } from './generated/errors/anchor';
/** /**
* Switchboard Devnet Program ID * Switchboard Devnet Program ID
@ -86,7 +89,7 @@ export class SwitchboardProgram {
bump: number; bump: number;
}; };
readonly mint: Mint; readonly mint: NativeMint;
/** /**
* Constructor. * Constructor.
@ -94,7 +97,7 @@ export class SwitchboardProgram {
constructor( constructor(
program: anchor.Program, program: anchor.Program,
cluster: Cluster | 'localnet', cluster: Cluster | 'localnet',
mint: Mint mint: NativeMint
) { ) {
this._program = program; this._program = program;
this.cluster = cluster; this.cluster = cluster;
@ -169,7 +172,9 @@ export class SwitchboardProgram {
payerKeypair, payerKeypair,
programId 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); return new SwitchboardProgram(program, cluster, mint);
}; };
@ -308,17 +313,37 @@ export class SwitchboardProgram {
s.publicKey.equals(txn.payer) || reqSigners.has(s.publicKey.toBase58()) s.publicKey.equals(txn.payer) || reqSigners.has(s.publicKey.toBase58())
); );
const txnSignature = await this.provider.sendAndConfirm( const transaction = txn.toTxn(
txn.toTxn(blockhash ?? (await this.connection.getLatestBlockhash())), blockhash ?? (await this.connection.getLatestBlockhash())
filteredSigners,
{
skipPreflight: false,
maxRetries: 10,
...opts,
}
); );
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 */ /* eslint-disable no-unused-vars */
import 'mocha'; import 'mocha';
import chai, { expect } from 'chai';
import assert from 'assert';
import * as sbv2 from '../src'; import * as sbv2 from '../src';
import { setupTest, TestContext } from './utilts'; import { setupTest, TestContext } from './utilts';
@ -189,9 +187,8 @@ describe('Aggregator Tests', () => {
} }
const aggregatorAccount = fundedAggregator; const aggregatorAccount = fundedAggregator;
const initialUserTokenBalance = await ctx.program.mint.getBalance( const initialUserTokenBalance =
ctx.payer.publicKey (await ctx.program.mint.getBalance(ctx.payer.publicKey)) ?? 0;
);
const [leaseAccount] = LeaseAccount.fromSeed( const [leaseAccount] = LeaseAccount.fromSeed(
ctx.program, 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 = const finalUserTokenBalance =
(await ctx.program.mint.getBalance(ctx.payer.publicKey)) ?? 0; (await ctx.program.mint.getBalance(ctx.payer.publicKey)) ?? 0;
if (initialUserTokenBalance !== finalUserTokenBalance) { if (initialUserTokenBalance !== finalUserTokenBalance) {

View File

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

View File

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

View File

@ -1,10 +1,18 @@
import 'mocha'; import 'mocha';
import assert from 'assert';
import * as sbv2 from '../src'; import * as sbv2 from '../src';
import { setupTest, TestContext } from './utilts'; import { setupTest, TestContext } from './utilts';
import { Keypair } from '@solana/web3.js'; 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 { OracleJob } from '@switchboard-xyz/common';
import { PermitOracleQueueUsage } from '../src/generated/types/SwitchboardPermission';
describe('Open Round Tests', () => { describe('Open Round Tests', () => {
let ctx: TestContext; let ctx: TestContext;
@ -15,17 +23,18 @@ describe('Open Round Tests', () => {
let queueAccount: QueueAccount; let queueAccount: QueueAccount;
let queue: types.OracleQueueAccountData; let queue: types.OracleQueueAccountData;
let createOracleSignature1: string; let createOracleSignature1: string[];
let oracleAccount1: OracleAccount; let oracleAccount1: OracleAccount;
let oracle1: types.OracleAccountData; let oracle1: types.OracleAccountData;
let createOracleSignature2: string; let createOracleSignature2: string[];
let oracleAccount2: OracleAccount; let oracleAccount2: OracleAccount;
let oracle2: types.OracleAccountData; let oracle2: types.OracleAccountData;
let createAggregatorSignatures: string[]; let createAggregatorSignatures: string[];
let aggregatorAccount: AggregatorAccount; let aggregatorAccount: AggregatorAccount;
let aggregator: types.AggregatorAccountData; let aggregator: types.AggregatorAccountData;
let aggregatorPermissionAccount: PermissionAccount;
before(async () => { before(async () => {
ctx = await setupTest(); ctx = await setupTest();
@ -52,6 +61,7 @@ describe('Open Round Tests', () => {
name: 'oracle-1', name: 'oracle-1',
metadata: 'oracle-1', metadata: 'oracle-1',
queueAuthority, queueAuthority,
enable: true,
}); });
oracle1 = await oracleAccount1.loadData(); oracle1 = await oracleAccount1.loadData();
@ -59,18 +69,18 @@ describe('Open Round Tests', () => {
name: 'oracle-2', name: 'oracle-2',
metadata: 'oracle-2', metadata: 'oracle-2',
queueAuthority, queueAuthority,
enable: true,
}); });
oracle2 = await oracleAccount2.loadData(); oracle2 = await oracleAccount2.loadData();
[aggregatorAccount, createAggregatorSignatures] = [aggregatorAccount, createAggregatorSignatures] =
await queueAccount.createFeed({ await queueAccount.createFeed({
queueAuthority: queueAuthority, batchSize: 2,
batchSize: 1, minRequiredOracleResults: 2,
minRequiredOracleResults: 1,
minRequiredJobResults: 1, minRequiredJobResults: 1,
minUpdateDelaySeconds: 60, minUpdateDelaySeconds: 5,
fundAmount: 1, fundAmount: 1,
enable: true, enable: false,
jobs: [ jobs: [
{ {
weight: 2, 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 'mocha';
import chai, { expect } from 'chai';
import assert from 'assert'; import assert from 'assert';
import * as sbv2 from '../src'; import * as sbv2 from '../src';
@ -46,6 +45,7 @@ describe('Queue Tests', () => {
queueAuthority, queueAuthority,
enable: true, enable: true,
authority: oracleAuthority, authority: oracleAuthority,
stakeAmount: 2,
}); });
const oracle = await oracleAccount.loadData(); const oracle = await oracleAccount.loadData();

View File

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