Last active
February 3, 2026 14:11
-
-
Save mgild/e8269c7c569eafd98260b0940859b05d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env npx tsx | |
| /** | |
| * Sui Feed Crank Script | |
| * | |
| * Fetches oracle responses and submits a feed update transaction. | |
| * | |
| * Usage: | |
| * npx tsx scripts/sui-feed-crank.ts <feed_id> [options] | |
| * | |
| * Arguments: | |
| * feed_id The Sui address of the feed aggregator | |
| * | |
| * Options: | |
| * --network Network to use: mainnet (default) or testnet | |
| * --num-sigs Number of oracle signatures to fetch (default: 1) | |
| * --crossbar Crossbar URL (default: https://crossbar.switchboard.xyz) | |
| * --key-file Path to private key file (default: test-key.txt) | |
| */ | |
| import * as fs from 'fs'; | |
| import * as path from 'path'; | |
| import { SuiClient } from '@mysten/sui/client'; | |
| import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; | |
| import { Transaction } from '@mysten/sui/transactions'; | |
| import { CrossbarClient } from '@switchboard-xyz/common'; | |
| import { Big, BN } from '@switchboard-xyz/common'; | |
| import { | |
| Aggregator, | |
| SwitchboardClient, | |
| ON_DEMAND_MAINNET_OBJECT_PACKAGE_ID, | |
| ON_DEMAND_TESTNET_OBJECT_PACKAGE_ID, | |
| } from '@switchboard-xyz/sui-sdk'; | |
| const DEFAULT_CROSSBAR_URL = 'https://crossbar.switchboard.xyz'; | |
| const SOLANA_MAINNET_RPC = 'https://api.mainnet-beta.solana.com'; | |
| const SOLANA_DEVNET_RPC = 'https://api.devnet.solana.com'; | |
| const SUI_NETWORKS = { | |
| mainnet: { | |
| rpcUrl: 'https://mainnet.sui.rpcpool.com', | |
| switchboardAddress: ON_DEMAND_MAINNET_OBJECT_PACKAGE_ID, | |
| solanaRpcUrl: SOLANA_MAINNET_RPC, | |
| explorerUrl: (txn: string) => `https://suiscan.xyz/mainnet/tx/${txn}`, | |
| }, | |
| testnet: { | |
| rpcUrl: 'https://testnet.sui.rpcpool.com', | |
| switchboardAddress: ON_DEMAND_TESTNET_OBJECT_PACKAGE_ID, | |
| solanaRpcUrl: SOLANA_DEVNET_RPC, | |
| explorerUrl: (txn: string) => `https://suiscan.xyz/testnet/tx/${txn}`, | |
| }, | |
| } as const; | |
| type Network = keyof typeof SUI_NETWORKS; | |
| interface Options { | |
| feedId: string; | |
| network: Network; | |
| numSignatures: number; | |
| crossbarUrl: string; | |
| keyFile: string; | |
| } | |
| function parseArgs(): Options { | |
| const args = process.argv.slice(2); | |
| if (args.length === 0 || args.includes('--help') || args.includes('-h')) { | |
| console.log(` | |
| Sui Feed Crank Script | |
| Fetches oracle responses and submits a feed update transaction. | |
| Usage: | |
| npx tsx scripts/sui-feed-crank.ts <feed_id> [options] | |
| Arguments: | |
| feed_id The Sui address of the feed aggregator | |
| Options: | |
| --network <net> Network to use: mainnet (default) or testnet | |
| --num-sigs <n> Number of oracle signatures to fetch (default: 1) | |
| --crossbar <url> Crossbar URL (default: https://crossbar.switchboard.xyz) | |
| --key-file <path> Path to private key file (default: test-key.txt) | |
| --help, -h Show this help message | |
| Example: | |
| npx tsx scripts/sui-feed-crank.ts 0x1234...abcd | |
| npx tsx scripts/sui-feed-crank.ts 0x1234...abcd --crossbar https://custom.crossbar.xyz | |
| `); | |
| process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); | |
| } | |
| const feedId = args[0]; | |
| let network: Network = 'mainnet'; | |
| let numSignatures = 1; | |
| let crossbarUrl = DEFAULT_CROSSBAR_URL; | |
| let keyFile = 'test-key.txt'; | |
| for (let i = 1; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (arg === '--network' && args[i + 1]) { | |
| const net = args[++i]; | |
| if (net !== 'mainnet' && net !== 'testnet') { | |
| console.error(`Invalid network: ${net}. Must be 'mainnet' or 'testnet'`); | |
| process.exit(1); | |
| } | |
| network = net; | |
| } else if (arg === '--num-sigs' && args[i + 1]) { | |
| numSignatures = parseInt(args[++i], 10); | |
| if (isNaN(numSignatures) || numSignatures < 1) { | |
| console.error('--num-sigs must be a positive integer'); | |
| process.exit(1); | |
| } | |
| } else if (arg === '--crossbar' && args[i + 1]) { | |
| crossbarUrl = args[++i]; | |
| } else if (arg === '--key-file' && args[i + 1]) { | |
| keyFile = args[++i]; | |
| } | |
| } | |
| return { feedId, network, numSignatures, crossbarUrl, keyFile }; | |
| } | |
| function loadKeypair(keyFile: string): Ed25519Keypair { | |
| const keyPath = path.resolve(process.cwd(), keyFile); | |
| if (!fs.existsSync(keyPath)) { | |
| console.error(`Key file not found: ${keyPath}`); | |
| process.exit(1); | |
| } | |
| const keyData = fs.readFileSync(keyPath, 'utf-8').trim(); | |
| // Try parsing as base64 secret key first | |
| try { | |
| const secretKey = Buffer.from(keyData, 'base64'); | |
| if (secretKey.length === 32 || secretKey.length === 64) { | |
| return Ed25519Keypair.fromSecretKey(secretKey.slice(0, 32)); | |
| } | |
| } catch {} | |
| // Try parsing as hex | |
| try { | |
| const secretKey = Buffer.from(keyData.replace(/^0x/, ''), 'hex'); | |
| if (secretKey.length === 32 || secretKey.length === 64) { | |
| return Ed25519Keypair.fromSecretKey(secretKey.slice(0, 32)); | |
| } | |
| } catch {} | |
| // Try parsing as Sui keystore format (suiprivkey1...) | |
| try { | |
| if (keyData.startsWith('suiprivkey')) { | |
| return Ed25519Keypair.fromSecretKey(keyData); | |
| } | |
| } catch {} | |
| console.error('Could not parse key file. Expected base64, hex, or suiprivkey format.'); | |
| process.exit(1); | |
| } | |
| function formatValue(value: BN): string { | |
| return new Big(value.toString()).div(1e18).toFixed(6); | |
| } | |
| async function main() { | |
| const options = parseArgs(); | |
| const config = SUI_NETWORKS[options.network]; | |
| console.log('='.repeat(60)); | |
| console.log('Sui Feed Crank'); | |
| console.log('='.repeat(60)); | |
| console.log(`Feed ID: ${options.feedId}`); | |
| console.log(`Network: ${options.network}`); | |
| console.log(`Crossbar: ${options.crossbarUrl}`); | |
| console.log(`Signatures: ${options.numSignatures}`); | |
| console.log(`Key File: ${options.keyFile}`); | |
| console.log('='.repeat(60)); | |
| console.log(); | |
| // Load keypair | |
| console.log('Loading keypair...'); | |
| const keypair = loadKeypair(options.keyFile); | |
| const senderAddress = keypair.getPublicKey().toSuiAddress(); | |
| console.log(`Sender: ${senderAddress}`); | |
| console.log(); | |
| // Initialize clients | |
| console.log('Initializing clients...'); | |
| const suiClient = new SuiClient({ url: config.rpcUrl }); | |
| const sbClient = new SwitchboardClient(suiClient); | |
| const crossbarClient = new CrossbarClient(options.crossbarUrl, true); | |
| const aggregator = new Aggregator(sbClient, options.feedId); | |
| // Load feed data | |
| console.log('Loading feed data...'); | |
| const aggregatorData = await aggregator.loadData().catch(err => { | |
| console.error(`Failed to load feed: ${err.message}`); | |
| process.exit(1); | |
| }); | |
| console.log(`Feed name: ${aggregatorData.name}`); | |
| console.log(`Feed hash: ${aggregatorData.feedHash}`); | |
| console.log(); | |
| // Fetch update | |
| console.log(`Fetching ${options.numSignatures} oracle response(s)...`); | |
| const transaction = new Transaction(); | |
| transaction.setSender(senderAddress); | |
| const startTime = Date.now(); | |
| const { responses } = await aggregator.fetchUpdateTx(transaction, { | |
| crossbarClient, | |
| crossbarUrl: options.crossbarUrl, | |
| solanaRPCUrl: config.solanaRpcUrl, | |
| feedConfigs: { ...aggregatorData, minSampleSize: options.numSignatures }, | |
| }); | |
| const fetchDuration = Date.now() - startTime; | |
| const responseResults = responses[0]?.results ?? []; | |
| const successfulResults = responseResults.filter(r => r.successValue); | |
| console.log(`Received ${successfulResults.length}/${options.numSignatures} responses in ${fetchDuration}ms`); | |
| if (successfulResults.length === 0) { | |
| console.error('No successful responses received'); | |
| process.exit(1); | |
| } | |
| // Print values | |
| console.log('\nOracle responses:'); | |
| for (const r of successfulResults) { | |
| console.log(` ${r.oracleId}: ${formatValue(new BN(r.successValue))}`); | |
| } | |
| // Calculate median | |
| const values = successfulResults.map(r => new Big(r.successValue.toString()).div(1e18)); | |
| const sorted = values.sort((a, b) => a.cmp(b)); | |
| const median = sorted[Math.floor(sorted.length / 2)]; | |
| console.log(`\nMedian value: ${median.toFixed(6)}`); | |
| console.log(); | |
| // Sign and submit transaction | |
| console.log('Signing and submitting transaction...'); | |
| const txStartTime = Date.now(); | |
| const result = await suiClient.signAndExecuteTransaction({ | |
| signer: keypair, | |
| transaction, | |
| options: { | |
| showEffects: true, | |
| showEvents: true, | |
| }, | |
| }); | |
| const txDuration = Date.now() - txStartTime; | |
| console.log(); | |
| console.log('='.repeat(60)); | |
| console.log('Transaction Result'); | |
| console.log('='.repeat(60)); | |
| console.log(`Digest: ${result.digest}`); | |
| console.log(`Status: ${result.effects?.status?.status ?? 'unknown'}`); | |
| console.log(`Gas used: ${result.effects?.gasUsed?.computationCost ?? 'unknown'}`); | |
| console.log(`Duration: ${txDuration}ms`); | |
| console.log(`Explorer: ${config.explorerUrl(result.digest)}`); | |
| console.log('='.repeat(60)); | |
| if (result.effects?.status?.status !== 'success') { | |
| console.error('Transaction failed:', result.effects?.status?.error); | |
| process.exit(1); | |
| } | |
| } | |
| main().catch(err => { | |
| console.error('Error:', err.message); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment