@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
301 lines (275 loc) • 8.26 kB
text/typescript
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);
}
});
});