UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

301 lines (275 loc) 8.26 kB
import { credentialToRewardAddress, Data, Emulator, EmulatorAccount, fromHex, generateEmulatorAccount, Lucid, scriptHashToCredential, slotToUnixTime, toHex, validatorToScriptHash, } from '@lucid-evolution/lucid'; import { beforeEach, describe, expect, test } from 'vitest'; import { LucidContext, runAndAwaitTxBuilder } from '../test-helpers'; import { initPyth, TEST_TRUSTED_SIGNER_PRIV_KEY, TEST_TRUSTED_SIGNER_PUB_KEY, } from './endpoints'; import { PythFeedParams, PythStateDatum, serialisePythFeedRedeemer, serialisePythStateDatum, serialisePythUpdatesRedeemer, toDataDerivedPythPrice, } from '../../src/contracts/pyth-feed/types'; import { assetClassToUnit } from '@3rd-eye-labs/cardano-offchain-common'; import { mkPythFeedValidator } from '../../src/contracts/pyth-feed/scripts'; import { alwaysSucceedValidator } from '../always-succeed/script'; import { encodePriceUpdate, encodePythMessage, type PriceUpdate, type PythMessageParts, } from '../../src/utils/pyth'; import { MAINNET_PROTOCOL_PARAMETERS } from '../indigo-test-helpers'; import { createPythMessage } from './helpers'; import { ParsedFeedPayload } from '@pythnetwork/pyth-lazer-sdk'; import { derivePythPrice } from '../../src/contracts/pyth-feed/helpers'; import * as Core from '@evolution-sdk/evolution'; type MyContext = LucidContext<{ admin: EmulatorAccount; }>; describe('Pyth > Initialization', () => { beforeEach<MyContext>(async (context: MyContext) => { context.users = { admin: generateEmulatorAccount({ lovelace: BigInt(100_000_000_000_000), }), }; context.emulator = new Emulator( [context.users.admin], MAINNET_PROTOCOL_PARAMETERS, ); context.lucid = await Lucid(context.emulator, 'Custom'); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); }); test('initPyth initializes the Pyth State NFT', async (context: MyContext) => { const pythStateAsset = await initPyth(context.lucid); expect(pythStateAsset).toBeDefined(); }); test('Properly serializes a Pyth State datum', (_context: MyContext) => { const pythStateDatum: PythStateDatum = { governance: { wormhole: fromHex(''), emitterChain: 0n, emitterAddress: fromHex(''), seenSequence: 0n, }, trustedSigners: new Map([ [ fromHex( '4a50d7c3d16b2be5c16ba996109a455a34cce08a81b3e15b86ef407e2f72e71f', ), { lower_bound: { bound_type: { _tag: 'NegativeInfinity', }, is_inclusive: true, }, upper_bound: { bound_type: { _tag: 'Finite', finite: 1n, }, is_inclusive: true, }, }, ], ]), deprecatedWithdrawScripts: new Map(), withdraw_script: fromHex( '7688145a4fa7aa18c16ef0daafeec18091ab99ea4268cfe2d863c79fda131938', ), }; const serialisedPythStateDatum = serialisePythStateDatum(pythStateDatum); expect(serialisedPythStateDatum).toBe( 'd8799fd8799f40004000ffa158204a50d7c3d16b2be5c16ba996109a455a34cce08a81b3e15b86ef407e2f72e71fd8799fd8799fd87980d87a80ffd8799fd87a9f01ffd87a80ffffa058207688145a4fa7aa18c16ef0daafeec18091ab99ea4268cfe2d863c79fda131938ff', ); }); test('Pyth Feed validator can confirm a Pyth Message', async (context: MyContext) => { const pythStateAsset = await initPyth(context.lucid); const noble = await import('@noble/ed25519'); const priceFeedId = 1; const price = 1000n; const exponent = -8; const params: PythFeedParams = { pythStatePolicyId: pythStateAsset.currencySymbol, config: { Value: { configuration: { priceFeedId: BigInt(priceFeedId) } }, }, }; const pfvValidator = mkPythFeedValidator(params); const pfvValHash = validatorToScriptHash(pfvValidator); // Register the stake key for pyth feed validator. await runAndAwaitTxBuilder( context.lucid, context.lucid.newTx().register.Stake( credentialToRewardAddress(context.lucid.config().network!, { hash: pfvValHash, type: 'Script', }), ), ); const currentTime = BigInt( slotToUnixTime(context.lucid.config().network!, context.emulator.slot), ); // This is to test the transaction validity when using a timestamp different than the // slot boundary (which is the usual and general case). const timestamp = currentTime - 10n; const update: PriceUpdate = { timestampUs: (timestamp * 1_000n).toString(), channelId: 0, priceFeeds: [ { priceFeedId, price: price.toString(), exponent, }, ], }; const payload = encodePriceUpdate(update); const signature = await noble.signAsync( payload, fromHex(TEST_TRUSTED_SIGNER_PRIV_KEY), ); const parts: PythMessageParts = { signature, publicKey: fromHex(TEST_TRUSTED_SIGNER_PUB_KEY), payload, }; const message = encodePythMessage(parts); const pythStateUtxo = await context.lucid.utxoByUnit( assetClassToUnit(pythStateAsset), ); await runAndAwaitTxBuilder( context.lucid, context.lucid .newTx() .readFrom([pythStateUtxo]) .attach.Script(pfvValidator) .validFrom(Number(timestamp)) .validTo(Number(timestamp) + 60 * 1000) .withdraw( credentialToRewardAddress( context.lucid.config().network!, scriptHashToCredential(pfvValHash), ), 0n, serialisePythFeedRedeemer({ price: { numerator: 1_000n, denominator: 100_000_000n, }, auxiliaryData: Core.Data.fromCBORHex(Data.void()), }), ) .withdraw( credentialToRewardAddress( context.lucid.config().network!, scriptHashToCredential( validatorToScriptHash(alwaysSucceedValidator), ), ), 0n, serialisePythUpdatesRedeemer([message]), ), ); }); }); describe('Pyth > Helper functions', () => { test('derivePythPrice works', async () => { const feed1: ParsedFeedPayload = { priceFeedId: 1, price: '1000', exponent: -8, }; const feed2: ParsedFeedPayload = { priceFeedId: 2, price: '99', exponent: 0, }; const feed3: ParsedFeedPayload = { priceFeedId: 3, price: '45', exponent: 0, }; const message = await createPythMessage( [feed1, feed2, feed3], BigInt(Date.now()), ); // Single direct value // 1000 * 10^-8 = 0.00001 { const price = derivePythPrice( { Value: { configuration: { priceFeedId: 1n }, }, }, toHex(message), ); expect(price.numerator).toBe(1_000n); expect(price.denominator).toBe(100_000_000n); } // Single inverse value: 1 / (1000 * 10^-8) = 1 / 0.00001 = 100000 { const price = derivePythPrice( { Inverse: { value: toDataDerivedPythPrice({ Value: { configuration: { priceFeedId: 1n, }, }, }), }, }, toHex(message), ); expect(price.numerator).toBe(100_000_000n); expect(price.denominator).toBe(1_000n); } // Single divide value: 99 / 45 = 2.2 { const price = derivePythPrice( { Divide: { x: toDataDerivedPythPrice({ Value: { configuration: { priceFeedId: 2n, }, }, }), y: toDataDerivedPythPrice({ Value: { configuration: { priceFeedId: 3n, }, }, }), }, }, toHex(message), ); expect(price.numerator).toBe(99n); expect(price.denominator).toBe(45n); } }); });