Skip to content

Commit bed1a5e

Browse files
authored
Merge pull request #42 from pyth-network/guibescos/anchor-client
Guibescos/anchor client
2 parents 05b4b3d + c31ac1b commit bed1a5e

File tree

16 files changed

+7656
-398
lines changed

16 files changed

+7656
-398
lines changed

package-lock.json

Lines changed: 6115 additions & 395 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@
2929
],
3030
"license": "Apache-2.0",
3131
"devDependencies": {
32+
"@coral-xyz/anchor": "^0.26.0",
3233
"@solana/web3.js": "^1.30.2",
33-
"@types/jest": "^26.0.23",
34+
"@types/bn.js": "^5.1.1",
35+
"@types/bs58": "^4.0.1",
36+
"@types/jest": "^27.0.7",
3437
"@types/node-fetch": "^2.6.2",
3538
"jest": "^27.3.1",
3639
"prettier": "^2.3.0",
@@ -43,6 +46,7 @@
4346
"buffer": "^6.0.1"
4447
},
4548
"peerDependencies": {
49+
"@coral-xyz/anchor": "^0.26.0",
4650
"@solana/web3.js": "^1.30.2"
4751
}
4852
}

src/__tests__/Anchor.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { AnchorProvider, Wallet } from '@coral-xyz/anchor'
2+
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
3+
import { BN } from 'bn.js'
4+
import { getPythProgramKeyForCluster, pythOracleProgram, PythOracleCoder } from '../index'
5+
6+
test('Anchor', (done) => {
7+
jest.setTimeout(60000)
8+
const provider = new AnchorProvider(
9+
new Connection('https://api.mainnet-beta.solana.com'),
10+
new Wallet(new Keypair()),
11+
AnchorProvider.defaultOptions(),
12+
)
13+
const pythOracle = pythOracleProgram(getPythProgramKeyForCluster('mainnet-beta'), provider)
14+
pythOracle.methods
15+
.initMapping()
16+
.accounts({ fundingAccount: PublicKey.unique(), freshMappingAccount: PublicKey.unique() })
17+
.instruction()
18+
.then((instruction) => {
19+
expect(instruction.data).toStrictEqual(Buffer.from([2, 0, 0, 0, 0, 0, 0, 0]))
20+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
21+
expect(decoded?.name).toBe('initMapping')
22+
expect(decoded?.data).toStrictEqual({})
23+
})
24+
pythOracle.methods
25+
.addMapping()
26+
.accounts({ fundingAccount: PublicKey.unique(), curMapping: PublicKey.unique(), nextMapping: PublicKey.unique() })
27+
.instruction()
28+
.then((instruction) => {
29+
expect(instruction.data).toStrictEqual(Buffer.from([2, 0, 0, 0, 1, 0, 0, 0]))
30+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
31+
expect(decoded?.name).toBe('addMapping')
32+
expect(decoded?.data).toStrictEqual({})
33+
})
34+
pythOracle.methods
35+
.updProduct()
36+
.accounts({ fundingAccount: PublicKey.unique(), productAccount: PublicKey.unique() })
37+
.instruction()
38+
.then((instruction) => {
39+
expect(instruction.data).toStrictEqual(Buffer.from([2, 0, 0, 0, 3, 0, 0, 0]))
40+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
41+
expect(decoded?.name).toBe('updProduct')
42+
expect(decoded?.data).toStrictEqual({})
43+
})
44+
45+
pythOracle.methods
46+
.addPrice(1, 1)
47+
.accounts({ fundingAccount: PublicKey.unique(), productAccount: PublicKey.unique() })
48+
.instruction()
49+
.then((instruction) => {
50+
expect(instruction.data).toStrictEqual(Buffer.from([2, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]))
51+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
52+
expect(decoded?.name).toBe('addPrice')
53+
expect(decoded?.data).toStrictEqual({ expo: 1, pType: 1 })
54+
})
55+
56+
pythOracle.methods
57+
.addPublisher(new PublicKey(5))
58+
.accounts({ fundingAccount: PublicKey.unique(), priceAccount: PublicKey.unique() })
59+
.instruction()
60+
.then((instruction) => {
61+
expect(instruction.data).toStrictEqual(
62+
Buffer.from([
63+
2, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
64+
0, 0, 5,
65+
]),
66+
)
67+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
68+
expect(decoded?.name).toBe('addPublisher')
69+
expect(decoded?.data.pub.equals(new PublicKey(5))).toBeTruthy()
70+
})
71+
72+
pythOracle.methods
73+
.updPrice(1, 0, new BN(42), new BN(9), new BN(1))
74+
.accounts({ fundingAccount: PublicKey.unique(), priceAccount: PublicKey.unique() })
75+
.instruction()
76+
.then((instruction) => {
77+
expect(instruction.data).toStrictEqual(
78+
Buffer.from([
79+
2, 0, 0, 0, 7, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
80+
0, 0, 0, 0,
81+
]),
82+
)
83+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
84+
expect(decoded?.name).toBe('updPrice')
85+
expect(decoded?.data.status === 1).toBeTruthy()
86+
expect(decoded?.data.price.eq(new BN(42))).toBeTruthy()
87+
expect(decoded?.data.conf.eq(new BN(9))).toBeTruthy()
88+
expect(decoded?.data.pubSlot.eq(new BN(1))).toBeTruthy()
89+
})
90+
91+
pythOracle.methods
92+
.updPriceNoFailOnError(1, 0, new BN(42), new BN(9), new BN(1))
93+
.accounts({ fundingAccount: PublicKey.unique(), priceAccount: PublicKey.unique() })
94+
.instruction()
95+
.then((instruction) => {
96+
expect(instruction.data).toStrictEqual(
97+
Buffer.from([
98+
2, 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
99+
0, 0, 0, 0,
100+
]),
101+
)
102+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
103+
expect(decoded?.name).toBe('updPriceNoFailOnError')
104+
expect(decoded?.data.status === 1).toBeTruthy()
105+
expect(decoded?.data.price.eq(new BN(42))).toBeTruthy()
106+
expect(decoded?.data.conf.eq(new BN(9))).toBeTruthy()
107+
expect(decoded?.data.pubSlot.eq(new BN(1))).toBeTruthy()
108+
})
109+
110+
pythOracle.methods
111+
.aggPrice(1, 0, new BN(42), new BN(9), new BN(1))
112+
.accounts({ fundingAccount: PublicKey.unique(), priceAccount: PublicKey.unique() })
113+
.instruction()
114+
.then((instruction) => {
115+
expect(instruction.data).toStrictEqual(
116+
Buffer.from([
117+
2, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
118+
0, 0, 0, 0,
119+
]),
120+
)
121+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
122+
expect(decoded?.name).toBe('aggPrice')
123+
expect(decoded?.data.status === 1).toBeTruthy()
124+
expect(decoded?.data.price.eq(new BN(42))).toBeTruthy()
125+
expect(decoded?.data.conf.eq(new BN(9))).toBeTruthy()
126+
expect(decoded?.data.pubSlot.eq(new BN(1))).toBeTruthy()
127+
})
128+
129+
pythOracle.methods
130+
.setMinPub(5, [0, 0, 0])
131+
.accounts({ fundingAccount: PublicKey.unique(), priceAccount: PublicKey.unique() })
132+
.instruction()
133+
.then((instruction) => {
134+
expect(instruction.data).toStrictEqual(Buffer.from([2, 0, 0, 0, 12, 0, 0, 0, 5, 0, 0, 0]))
135+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
136+
expect(decoded?.name).toBe('setMinPub')
137+
expect(decoded?.data.minPub === 5).toBeTruthy()
138+
})
139+
140+
pythOracle.methods
141+
.updPermissions(new PublicKey(6), new PublicKey(7), new PublicKey(8))
142+
.accounts({
143+
upgradeAuthority: PublicKey.unique(),
144+
programAccount: PublicKey.unique(),
145+
programDataAccount: PublicKey.unique(),
146+
})
147+
.instruction()
148+
.then((instruction) => {
149+
const expectedPda = PublicKey.findProgramAddressSync([Buffer.from('permissions')], pythOracle.programId)[0]
150+
expect(expectedPda.equals(instruction.keys[3].pubkey))
151+
expect(instruction.data).toStrictEqual(
152+
Buffer.from([
153+
2, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
154+
0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0,
155+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
156+
]),
157+
)
158+
const decoded = (pythOracle.coder as PythOracleCoder).instruction.decode(instruction.data)
159+
expect(decoded?.name).toBe('updPermissions')
160+
expect(decoded?.data.masterAuthority.equals(new PublicKey(6))).toBeTruthy()
161+
expect(decoded?.data.dataCurationAuthority.equals(new PublicKey(7))).toBeTruthy()
162+
expect(decoded?.data.securityAuthority.equals(new PublicKey(8))).toBeTruthy()
163+
})
164+
165+
done()
166+
})

src/anchor/coder/accounts.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { AccountsCoder, ACCOUNT_DISCRIMINATOR_SIZE, Idl } from '@coral-xyz/anchor'
2+
import { IdlTypeDef } from '@coral-xyz/anchor/dist/cjs/idl'
3+
4+
export class PythOracleAccountCoder<A extends string = string> implements AccountsCoder {
5+
constructor(private idl: Idl) {}
6+
7+
public async encode<T = any>(accountName: A, account: T): Promise<Buffer> {
8+
throw new Error('Not implemented')
9+
}
10+
11+
public decode<T = any>(accountName: A, ix: Buffer): T {
12+
throw new Error('Not implemented')
13+
}
14+
15+
public decodeUnchecked<T = any>(accountName: A, ix: Buffer): T {
16+
throw new Error('Not implemented')
17+
}
18+
19+
public memcmp(accountName: A, _appendData?: Buffer): { dataSize?: number; offset?: number; bytes?: string } {
20+
throw new Error('Not implemented')
21+
}
22+
23+
public size(idlAccount: IdlTypeDef): number {
24+
return 0
25+
}
26+
}

src/anchor/coder/events.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Idl, Event, EventCoder } from '@coral-xyz/anchor'
2+
import { IdlEvent } from '@coral-xyz/anchor/dist/cjs/idl'
3+
4+
export class PythOracleEventCoder implements EventCoder {
5+
decode<E extends IdlEvent = IdlEvent, T = Record<string, string>>(_log: string): Event<E, T> | null {
6+
throw new Error('Pyth oracle program does not have events')
7+
}
8+
}

src/anchor/coder/idl.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Borrowed from coral-xyz/anchor
2+
//
3+
// https://github.com/coral-xyz/anchor/blob/master/ts/packages/anchor/src/coder/borsh/idl.ts
4+
5+
import camelCase from 'camelcase'
6+
import { Layout } from 'buffer-layout'
7+
import * as borsh from '@coral-xyz/borsh'
8+
import { IdlEnumVariant, IdlField, IdlType, IdlTypeDef } from '@coral-xyz/anchor/dist/cjs/idl'
9+
import { IdlError } from '@coral-xyz/anchor'
10+
11+
export class IdlCoder {
12+
public static fieldLayout(field: { name?: string } & Pick<IdlField, 'type'>, types?: IdlTypeDef[]): Layout {
13+
const fieldName = field.name !== undefined ? camelCase(field.name) : undefined
14+
switch (field.type) {
15+
case 'bool': {
16+
return borsh.bool(fieldName)
17+
}
18+
case 'u8': {
19+
return borsh.u8(fieldName)
20+
}
21+
case 'i8': {
22+
return borsh.i8(fieldName)
23+
}
24+
case 'u16': {
25+
return borsh.u16(fieldName)
26+
}
27+
case 'i16': {
28+
return borsh.i16(fieldName)
29+
}
30+
case 'u32': {
31+
return borsh.u32(fieldName)
32+
}
33+
case 'i32': {
34+
return borsh.i32(fieldName)
35+
}
36+
case 'f32': {
37+
return borsh.f32(fieldName)
38+
}
39+
case 'u64': {
40+
return borsh.u64(fieldName)
41+
}
42+
case 'i64': {
43+
return borsh.i64(fieldName)
44+
}
45+
case 'f64': {
46+
return borsh.f64(fieldName)
47+
}
48+
case 'u128': {
49+
return borsh.u128(fieldName)
50+
}
51+
case 'i128': {
52+
return borsh.i128(fieldName)
53+
}
54+
case 'u256': {
55+
return borsh.u256(fieldName)
56+
}
57+
case 'i256': {
58+
return borsh.i256(fieldName)
59+
}
60+
case 'bytes': {
61+
return borsh.vecU8(fieldName)
62+
}
63+
case 'string': {
64+
return borsh.str(fieldName)
65+
}
66+
case 'publicKey': {
67+
return borsh.publicKey(fieldName)
68+
}
69+
default: {
70+
if ('vec' in field.type) {
71+
return borsh.vec(
72+
IdlCoder.fieldLayout(
73+
{
74+
name: undefined,
75+
type: field.type.vec,
76+
},
77+
types,
78+
),
79+
fieldName,
80+
)
81+
} else if ('option' in field.type) {
82+
return borsh.option(
83+
IdlCoder.fieldLayout(
84+
{
85+
name: undefined,
86+
type: field.type.option,
87+
},
88+
types,
89+
),
90+
fieldName,
91+
)
92+
} else if ('defined' in field.type) {
93+
const defined = field.type.defined
94+
// User defined type.
95+
if (types === undefined) {
96+
throw new IdlError('User defined types not provided')
97+
}
98+
const filtered = types.filter((t) => t.name === defined)
99+
if (filtered.length !== 1) {
100+
throw new IdlError(`Type not found: ${JSON.stringify(field)}`)
101+
}
102+
return IdlCoder.typeDefLayout(filtered[0], types, fieldName)
103+
} else if ('array' in field.type) {
104+
const arrayTy = field.type.array[0]
105+
const arrayLen = field.type.array[1]
106+
const innerLayout = IdlCoder.fieldLayout(
107+
{
108+
name: undefined,
109+
type: arrayTy,
110+
},
111+
types,
112+
)
113+
return borsh.array(innerLayout, arrayLen, fieldName)
114+
} else {
115+
throw new Error(`Not yet implemented: ${field}`)
116+
}
117+
}
118+
}
119+
}
120+
121+
public static typeDefLayout(typeDef: IdlTypeDef, types: IdlTypeDef[] = [], name?: string): Layout {
122+
if (typeDef.type.kind === 'struct') {
123+
const fieldLayouts = typeDef.type.fields.map((field) => {
124+
const x = IdlCoder.fieldLayout(field, types)
125+
return x
126+
})
127+
return borsh.struct(fieldLayouts, name)
128+
} else if (typeDef.type.kind === 'enum') {
129+
const variants = typeDef.type.variants.map((variant: IdlEnumVariant) => {
130+
const variantName = camelCase(variant.name)
131+
if (variant.fields === undefined) {
132+
return borsh.struct([], variantName)
133+
}
134+
const fieldLayouts = variant.fields.map((f: IdlField | IdlType, i: number) => {
135+
if (!f.hasOwnProperty('name')) {
136+
return IdlCoder.fieldLayout({ type: f as IdlType, name: i.toString() }, types)
137+
}
138+
// this typescript conversion is ok
139+
// because if f were of type IdlType
140+
// (that does not have a name property)
141+
// the check before would've errored
142+
return IdlCoder.fieldLayout(f as IdlField, types)
143+
})
144+
return borsh.struct(fieldLayouts, variantName)
145+
})
146+
147+
if (name !== undefined) {
148+
// Buffer-layout lib requires the name to be null (on construction)
149+
// when used as a field.
150+
return borsh.rustEnum(variants).replicate(name)
151+
}
152+
153+
return borsh.rustEnum(variants, name)
154+
} else {
155+
throw new Error(`Unknown type kint: ${typeDef}`)
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)