Skip to content

Instantly share code, notes, and snippets.

@mgild
Last active February 3, 2026 14:11
Show Gist options
  • Select an option

  • Save mgild/e8269c7c569eafd98260b0940859b05d to your computer and use it in GitHub Desktop.

Select an option

Save mgild/e8269c7c569eafd98260b0940859b05d to your computer and use it in GitHub Desktop.
#!/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