Skip to content
This repository has been archived by the owner on Feb 9, 2022. It is now read-only.

lambda-prevout-provider fallback-prevout-provider #128

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions packages/salmon-lambda-functions/src/generic/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { PriceManager, PriceProvider, AssetPrice } from '@defichain/salmon-price-functions'
import { OraclesManager } from '@defichain/salmon-oracles-functions'
import { Prevout } from '@defichain/jellyfish-transaction-builder/dist'
import { getEnvironmentConfig, EnvironmentConfig } from './environment'
import { checkBalanceAndNotify } from './slack'
import { LambdaPrevoutProvider } from './lambdaPrevoutProvider'

const broadcastPrices = async (oraclesManager: OraclesManager, env: EnvironmentConfig, prices: AssetPrice[]): Promise<string | undefined> => {
const broadcastPrices = async (oraclesManager: OraclesManager, env: EnvironmentConfig, prices: AssetPrice[]):
Promise<Prevout | undefined> => {
return await oraclesManager.updatePrices(env.oracleId,
prices.map(assetPrice => ({
token: assetPrice.asset, prices: [{ currency: env.currency, amount: assetPrice.price }]
Expand All @@ -21,12 +24,13 @@ export async function handleGenericPriceApiProvider (provider: PriceProvider, ev
const prices = await fetchPrices(env, provider)
console.log(JSON.stringify({ prices, event }))

const oraclesManager = OraclesManager.withWhaleClient(env.oceanUrl, env.network, env.privateKey)
const oraclesManager =
OraclesManager.withWhaleClient(env.oceanUrl, env.network, env.privateKey, new LambdaPrevoutProvider())

try {
const txid = await broadcastPrices(oraclesManager, env, prices)
if (txid !== undefined) {
console.log(`Sent with txid: ${txid}`)
const prevout = await broadcastPrices(oraclesManager, env, prices)
if (prevout !== undefined) {
process.stdout.write(`${JSON.stringify(prevout)}`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
process.stdout.write(`${JSON.stringify(prevout)}`)
process.stdout.write(`${FALLBACK_PREVOUT_PREFIX} ${JSON.stringify(prevout)}`)

With specific log group, literally we have partitioned, filterable time series DB :D

}
} finally {
// This gets called even if we escalate the exception, as
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Prevout, PrevoutProvider } from '@defichain/jellyfish-transaction-builder/dist'
import AWS from 'aws-sdk'
import BigNumber from 'bignumber.js'

const SEEK_MINUTES = 10
const VOUT_LOG_START = '\tINFO\t'
const isLambda = process.env.LAMBDA_TASK_ROOT !== undefined
const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME ?? ''
const logGroupPrefix = '/aws/lambda/'

function mapEventsToPrevouts (events: AWS.CloudWatchLogs.OutputLogEvents): Prevout[] {
return events.flatMap(event => {
try {
const message = event.message
if (message === undefined) {
return []
}

const startPos: number = message.indexOf(VOUT_LOG_START)
if (startPos === -1) {
return []
}

const trimmedMessage = message.substring(startPos + VOUT_LOG_START.length)
const prevout = JSON.parse(trimmedMessage)
if (prevout.vout !== undefined &&
prevout.txid !== undefined &&
prevout.value !== undefined &&
prevout.script !== undefined &&
prevout.tokenId !== undefined) {
return [prevout]
}
} catch {}

return []
})
}

async function resolvePrevoutsFromCloudWatchLogs (cloudwatchlogs: AWS.CloudWatchLogs,
groupParams: AWS.CloudWatchLogs.DescribeLogStreamsRequest): Promise<Prevout[]> {
return await new Promise<Prevout[]>((resolve, reject) => {
cloudwatchlogs.describeLogStreams(groupParams, (_, groupData) => {
const logStreams = groupData.logStreams
if (logStreams === undefined || logStreams.length === 0) {
reject(new Error('Log streams empty or undefined'))
return
}

const params: AWS.CloudWatchLogs.GetLogEventsRequest = {
logGroupName: groupParams.logGroupName,
logStreamName: logStreams[0].logStreamName ?? '',
startTime: Date.now() - (1000 * 60 * SEEK_MINUTES)
}

cloudwatchlogs.getLogEvents(params, (_, data) => {
const mapped = mapEventsToPrevouts(data.events ?? [])
if (mapped.length === 0) {
reject(new Error('No prevouts available'))
return
}

// Return strictly the latest prevout as upstream may
// choose in any order
resolve(mapped.slice(-1))
})
})
})
}

export class LambdaPrevoutProvider implements PrevoutProvider {
async all (): Promise<Prevout[]> {
if (!isLambda) {
return []
}

const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' })
const groupParams: AWS.CloudWatchLogs.DescribeLogStreamsRequest = {
logGroupName: `${logGroupPrefix}${functionName}`,
descending: true,
orderBy: 'LastEventTime'
}
return await resolvePrevoutsFromCloudWatchLogs(cloudwatchlogs, groupParams)
}

async collect (_: BigNumber): Promise<Prevout[]> {
// TODO(fuxingloh): min balance filtering
return await this.all()
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MasterNodeRegTestContainer, GenesisKeys } from '@defichain/testcontainers'
import { fundEllipticPair, sendTransaction } from './test.utils'
import { getProviders, MockProviders, MockWalletAccount } from './provider.mock'
import { OraclesManager } from '../src'
import { getProviders, MockPrevoutListProvider, MockProviders, MockWalletAccount } from './provider.mock'
import { FallbackPrevoutProvider, OraclesManager } from '../src'
import { dSHA256, WIF } from '@defichain/jellyfish-crypto'
import { P2WPKHTransactionBuilder } from '@defichain/jellyfish-transaction-builder/dist'
import { P2WPKHTransactionBuilder, Prevout } from '@defichain/jellyfish-transaction-builder/dist'
import { SmartBuffer } from 'smart-buffer'
import { BigNumber } from 'bignumber.js'
import { CTransaction, Transaction } from '@defichain/jellyfish-transaction'
Expand Down Expand Up @@ -112,4 +112,55 @@ describe('basic price oracles', () => {

expect(broadcastMock).not.toHaveBeenCalled()
})

it('should chain prevouts using fallback provider', async () => {
const prevoutListProvider = new MockPrevoutListProvider()

const oraclesManager = new OraclesManager(
async rawTx => {
const txid = await container.call('sendrawtransaction', [rawTx.hex])
await container.generate(1)
return txid
},
new P2WPKHTransactionBuilder(providers.fee, new FallbackPrevoutProvider(providers.prevout, prevoutListProvider), {
get: (_) => providers.ellipticPair
}),
new MockWalletAccount(new WalletClassic(providers.ellipticPair))
)

// Appoint Oracle
const script = await oraclesManager.getChangeScript()
const appointTxn = await builder.oracles.appointOracle({
script: script,
weightage: 1,
priceFeeds: [
{
token: 'TEST',
currency: 'USD'
}
]
}, script)

await sendTransaction(container, appointTxn)

const oracleId = calculateTxid(appointTxn)
const prevout: Prevout | undefined = await oraclesManager.updatePrices(oracleId, [{ token: 'TEST', prices: [{ currency: 'USD', amount: new BigNumber(0.1) }] }])
expect(prevout).not.toBe(undefined)

// Ensure oracle is updated and has correct values
const getOracleDataResult = await container.call('getoracledata', [oracleId])
expect(getOracleDataResult.priceFeeds.length).toStrictEqual(1)
expect(getOracleDataResult.priceFeeds[0].token).toStrictEqual('TEST')
expect(getOracleDataResult.priceFeeds[0].currency).toStrictEqual('USD')
expect(getOracleDataResult.tokenPrices[0].token).toStrictEqual('TEST')
expect(getOracleDataResult.tokenPrices[0].currency).toStrictEqual('USD')
expect(getOracleDataResult.tokenPrices[0].amount).toStrictEqual(0.1)

// Send with prevout added to list
prevoutListProvider.prevoutList = prevout !== undefined ? [prevout] : []
expect(prevoutListProvider.prevoutList.length).toStrictEqual(1)
await oraclesManager.updatePrices(oracleId, [{ token: 'TEST', prices: [{ currency: 'USD', amount: new BigNumber(0.2) }] }])
const getOracleDataResult2 = await container.call('getoracledata', [oracleId])
expect(getOracleDataResult2.tokenPrices[0].amount).toStrictEqual(0.2)
})
})
13 changes: 13 additions & 0 deletions packages/salmon-oracles-functions/__tests__/provider.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,16 @@ export class MockWalletAccount extends WalletAccount {
return true
}
}

export class MockPrevoutListProvider implements PrevoutProvider {
public prevoutList: Prevout[] = []

async all (): Promise<Prevout[]> {
return this.prevoutList
}

async collect (_: BigNumber): Promise<Prevout[]> {
// TODO(fuxingloh): min balance filtering
return await this.all()
}
}
1 change: 1 addition & 0 deletions packages/salmon-oracles-functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './oraclesManager'
export * from './salmonWalletAccount'
23 changes: 15 additions & 8 deletions packages/salmon-oracles-functions/src/oraclesManager.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { P2WPKHTransactionBuilder } from '@defichain/jellyfish-transaction-builder'
import { P2WPKHTransactionBuilder, Prevout, PrevoutProvider } from '@defichain/jellyfish-transaction-builder'
import { WIF } from '@defichain/jellyfish-crypto'
import { CTransactionSegWit, Script, TokenPrice, TransactionSegWit } from '@defichain/jellyfish-transaction'
import { WhaleApiClient } from '@defichain/whale-api-client'
import { WhaleWalletAccount } from '@defichain/whale-api-wallet'
import { getNetwork, NetworkName } from '@defichain/jellyfish-network'
import BigNumber from 'bignumber.js'
import { WalletAccount } from '@defichain/jellyfish-wallet'
import { WalletClassic } from '@defichain/jellyfish-wallet-classic'

import { SalmonWalletAccount } from './salmonWalletAccount'
export class OraclesManager {
constructor (
private readonly broadcastHex: (rawTx: { hex: string }) => Promise<string>,
Expand Down Expand Up @@ -38,7 +37,7 @@ export class OraclesManager {
oracleId: string,
tokenPrices: TokenPrice[],
timestamp: BigNumber = new BigNumber(Math.floor(Date.now() / 1000))
): Promise<string | undefined> {
): Promise<Prevout | undefined> {
if (tokenPrices.length === 0) {
return
}
Expand All @@ -51,7 +50,14 @@ export class OraclesManager {

const setOracleDataTxn: TransactionSegWit = await this.builder.oracles.setOracleData(txnData, await this.getChangeScript())
const transactionSegWit: CTransactionSegWit = new CTransactionSegWit(setOracleDataTxn)
return await this.broadcastHex({ hex: transactionSegWit.toHex() })
const voutInfo = transactionSegWit.vout[1]
const txid = await this.broadcastHex({ hex: transactionSegWit.toHex() })

return {
txid,
vout: 1,
...voutInfo
}
}

/**
Expand All @@ -65,16 +71,17 @@ export class OraclesManager {
static withWhaleClient (
url: string,
network: string,
privKey: string
privKey: string,
preferredPrevoutProvider?: PrevoutProvider
): OraclesManager {
const whaleClient = new WhaleApiClient({
url,
network
})

const hdNode = new WalletClassic(WIF.asEllipticPair(privKey))
const walletAccount = new WhaleWalletAccount(whaleClient, hdNode,
getNetwork(network as NetworkName))
const walletAccount = new SalmonWalletAccount(whaleClient, hdNode,
getNetwork(network as NetworkName), preferredPrevoutProvider)

return new OraclesManager(
async (rawTx: { hex: string }): Promise<string> =>
Expand Down
58 changes: 58 additions & 0 deletions packages/salmon-oracles-functions/src/salmonWalletAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { WalletEllipticPair } from '@defichain/jellyfish-wallet'
import { WhaleApiClient } from '@defichain/whale-api-client'
import { WhaleWalletAccount } from '@defichain/whale-api-wallet'
import { Network } from '@defichain/jellyfish-network'
import { P2WPKHTransactionBuilder, Prevout, PrevoutProvider } from '@defichain/jellyfish-transaction-builder/dist'
import BigNumber from 'bignumber.js'

/**
* FallbackPrevoutProvider provides an abstraction that takes
* a fallback prevout provider, and a preferred prevout provider.
* If the preferred provider is empty or throws an error, the fallback
* provider is used.
*/
export class FallbackPrevoutProvider implements PrevoutProvider {
constructor (
protected readonly fallbackPrevoutProvider: PrevoutProvider,
protected readonly preferredPrevoutProvider?: PrevoutProvider
) {
}

async all (): Promise<Prevout[]> {
if (this.preferredPrevoutProvider !== undefined) {
try {
const prevouts: Prevout[] = await this.preferredPrevoutProvider.all()
if (prevouts.length > 0) {
return prevouts
}
} catch {}
}

return await this.fallbackPrevoutProvider.all()
}

async collect (_: BigNumber): Promise<Prevout[]> {
// TODO(fuxingloh): min balance filtering
return await this.all()
}
}

export class SalmonWalletAccount extends WhaleWalletAccount {
protected readonly salmonPrevoutProvider: PrevoutProvider

constructor (
public readonly client: WhaleApiClient,
walletEllipticPair: WalletEllipticPair,
network: Network,
preferredPrevoutProvider?: PrevoutProvider
) {
super(client, walletEllipticPair, network, 10)
this.salmonPrevoutProvider = new FallbackPrevoutProvider(this.prevoutProvider, preferredPrevoutProvider)
}

withTransactionBuilder (): P2WPKHTransactionBuilder {
return new P2WPKHTransactionBuilder(this.feeRateProvider, this.salmonPrevoutProvider, {
get: (_) => this
})
}
}