Skip to content

Commit 050b827

Browse files
authored
feat(price_pusher): solana price pusher (#1408)
* Checkpoint * Checkpoint * Checkpoint * Checkpoint * fix: pusher * Checkpoint * Works * fix: pass pusher program id * Add docs * 0.1.0 * Bump npm package * Go * Comment * Add customizable shard id * Allow configurable priority fees
1 parent 450a483 commit 050b827

File tree

12 files changed

+671
-7
lines changed

12 files changed

+671
-7
lines changed

package-lock.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"endpoint": "https://api.devnet.solana.com",
3+
"keypair-file": "/keypair"
4+
}

price_pusher/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
},
5353
"dependencies": {
5454
"@injectivelabs/sdk-ts": "1.10.72",
55+
"@pythnetwork/pyth-solana-receiver": "*",
5556
"@mysten/sui.js": "^0.49.1",
5657
"@pythnetwork/price-service-client": "*",
5758
"@pythnetwork/pyth-sdk-solidity": "*",

price_pusher/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import evm from "./evm/command";
66
import aptos from "./aptos/command";
77
import sui from "./sui/command";
88
import near from "./near/command";
9+
import solana from "./solana/command";
910

1011
yargs(hideBin(process.argv))
1112
.config("config")
@@ -15,4 +16,5 @@ yargs(hideBin(process.argv))
1516
.command(aptos)
1617
.command(sui)
1718
.command(near)
19+
.command(solana)
1820
.help().argv;

price_pusher/src/solana/command.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Options } from "yargs";
2+
import * as options from "../options";
3+
import { readPriceConfigFile } from "../price-config";
4+
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
5+
import { PythPriceListener } from "../pyth-price-listener";
6+
import { SolanaPriceListener, SolanaPricePusher } from "./solana";
7+
import { Controller } from "../controller";
8+
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
9+
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
10+
import { Keypair, Connection } from "@solana/web3.js";
11+
import fs from "fs";
12+
import { PublicKey } from "@solana/web3.js";
13+
14+
export default {
15+
command: "solana",
16+
describe: "run price pusher for solana",
17+
builder: {
18+
endpoint: {
19+
description: "Solana RPC API endpoint",
20+
type: "string",
21+
required: true,
22+
} as Options,
23+
"keypair-file": {
24+
description: "Path to a keypair file",
25+
type: "string",
26+
required: true,
27+
} as Options,
28+
"shard-id": {
29+
description: "Shard ID",
30+
type: "number",
31+
required: true,
32+
} as Options,
33+
"compute-unit-price-micro-lamports": {
34+
description: "Priority fee per compute unit",
35+
type: "number",
36+
default: 50000,
37+
} as Options,
38+
...options.priceConfigFile,
39+
...options.priceServiceEndpoint,
40+
...options.pythContractAddress,
41+
...options.pollingFrequency,
42+
...options.pushingFrequency,
43+
},
44+
handler: function (argv: any) {
45+
const {
46+
endpoint,
47+
keypairFile,
48+
shardId,
49+
computeUnitPriceMicroLamports,
50+
priceConfigFile,
51+
priceServiceEndpoint,
52+
pythContractAddress,
53+
pushingFrequency,
54+
pollingFrequency,
55+
} = argv;
56+
57+
const priceConfigs = readPriceConfigFile(priceConfigFile);
58+
59+
const priceServiceConnection = new PriceServiceConnection(
60+
priceServiceEndpoint,
61+
{
62+
logger: {
63+
// Log only warnings and errors from the price service client
64+
info: () => undefined,
65+
warn: console.warn,
66+
error: console.error,
67+
debug: () => undefined,
68+
trace: () => undefined,
69+
},
70+
}
71+
);
72+
73+
const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
74+
75+
const pythListener = new PythPriceListener(
76+
priceServiceConnection,
77+
priceItems
78+
);
79+
80+
const wallet = new NodeWallet(
81+
Keypair.fromSecretKey(
82+
Uint8Array.from(JSON.parse(fs.readFileSync(keypairFile, "ascii")))
83+
)
84+
);
85+
86+
const pythSolanaReceiver = new PythSolanaReceiver({
87+
connection: new Connection(endpoint),
88+
wallet,
89+
pushOracleProgramId: new PublicKey(pythContractAddress),
90+
});
91+
92+
const solanaPricePusher = new SolanaPricePusher(
93+
pythSolanaReceiver,
94+
priceServiceConnection,
95+
shardId,
96+
computeUnitPriceMicroLamports
97+
);
98+
const solanaPriceListener = new SolanaPriceListener(
99+
pythSolanaReceiver,
100+
shardId,
101+
priceItems,
102+
{ pollingFrequency }
103+
);
104+
105+
const controller = new Controller(
106+
priceConfigs,
107+
pythListener,
108+
solanaPriceListener,
109+
solanaPricePusher,
110+
{ pushingFrequency }
111+
);
112+
113+
controller.start();
114+
},
115+
};

price_pusher/src/solana/solana.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
2+
import {
3+
ChainPriceListener,
4+
IPricePusher,
5+
PriceInfo,
6+
PriceItem,
7+
} from "../interface";
8+
import { DurationInSeconds } from "../utils";
9+
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
10+
11+
export class SolanaPriceListener extends ChainPriceListener {
12+
constructor(
13+
private pythSolanaReceiver: PythSolanaReceiver,
14+
private shardId: number,
15+
priceItems: PriceItem[],
16+
config: {
17+
pollingFrequency: DurationInSeconds;
18+
}
19+
) {
20+
super("solana", config.pollingFrequency, priceItems);
21+
}
22+
23+
async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
24+
try {
25+
const priceFeedAccount =
26+
await this.pythSolanaReceiver.fetchPriceFeedAccount(
27+
this.shardId,
28+
Buffer.from(priceId, "hex")
29+
);
30+
console.log(
31+
`Polled a Solana on chain price for feed ${this.priceIdToAlias.get(
32+
priceId
33+
)} (${priceId}).`
34+
);
35+
if (priceFeedAccount) {
36+
return {
37+
conf: priceFeedAccount.priceMessage.conf.toString(),
38+
price: priceFeedAccount.priceMessage.price.toString(),
39+
publishTime: priceFeedAccount.priceMessage.publishTime.toNumber(),
40+
};
41+
} else {
42+
return undefined;
43+
}
44+
} catch (e) {
45+
console.error(`Polling on-chain price for ${priceId} failed. Error:`);
46+
console.error(e);
47+
return undefined;
48+
}
49+
}
50+
}
51+
52+
export class SolanaPricePusher implements IPricePusher {
53+
constructor(
54+
private pythSolanaReceiver: PythSolanaReceiver,
55+
private priceServiceConnection: PriceServiceConnection,
56+
private shardId: number,
57+
private computeUnitPriceMicroLamports: number
58+
) {}
59+
60+
async updatePriceFeed(
61+
priceIds: string[],
62+
pubTimesToPush: number[]
63+
): Promise<void> {
64+
if (priceIds.length === 0) {
65+
return;
66+
}
67+
68+
let priceFeedUpdateData;
69+
try {
70+
priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
71+
priceIds
72+
);
73+
} catch (e: any) {
74+
console.error(new Date(), "getPriceFeedsUpdateData failed:", e);
75+
return;
76+
}
77+
78+
const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({
79+
closeUpdateAccounts: false,
80+
});
81+
transactionBuilder.addUpdatePriceFeed(priceFeedUpdateData, this.shardId);
82+
83+
try {
84+
await this.pythSolanaReceiver.provider.sendAll(
85+
await transactionBuilder.buildVersionedTransactions({
86+
computeUnitPriceMicroLamports: this.computeUnitPriceMicroLamports,
87+
})
88+
);
89+
console.log(new Date(), "updatePriceFeed successful");
90+
} catch (e: any) {
91+
console.error(new Date(), "updatePriceFeed failed", e);
92+
return;
93+
}
94+
}
95+
}

target_chains/solana/programs/pyth-push-oracle/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,14 @@ pub struct UpdatePriceFeed<'info> {
8989
#[account(mut)]
9090
pub payer: Signer<'info>,
9191
pub pyth_solana_receiver: Program<'info, PythSolanaReceiver>,
92+
/// CHECK: Checked by CPI into the Pyth Solana Receiver
9293
pub encoded_vaa: AccountInfo<'info>,
94+
/// CHECK: Checked by CPI into the Pyth Solana Receiver
9395
pub config: AccountInfo<'info>,
96+
/// CHECK: Checked by CPI into the Pyth Solana Receiver
9497
#[account(mut)]
9598
pub treasury: AccountInfo<'info>,
99+
/// CHECK: This account's seeds are checked
96100
#[account(mut, seeds = [&shard_id.to_le_bytes(), &feed_id], bump)]
97101
pub price_feed_account: AccountInfo<'info>,
98102
pub system_program: Program<'info, System>,

target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ pub mod price_update;
1212

1313
declare_id!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ");
1414

15-
pub const PYTH_PUSH_ORACLE_ID: Pubkey = pubkey!("F9SP6tBXw9Af7BYauo7Y2R5Es2mpv8FP5aNCXMihp6Za");
15+
pub const PYTH_PUSH_ORACLE_ID: Pubkey = pubkey!("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT");

target_chains/solana/sdk/js/pyth_solana_receiver/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/pyth-solana-receiver",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "Pyth solana receiver SDK",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",

0 commit comments

Comments
 (0)