diff --git a/src/formulas/formulas/contract/common.ts b/src/formulas/formulas/contract/common.ts index 0af9645e..750fe8e8 100644 --- a/src/formulas/formulas/contract/common.ts +++ b/src/formulas/formulas/contract/common.ts @@ -1,6 +1,7 @@ import { ContractFormula } from '@/types' import { ContractInfo } from '../types' +import { makeSimpleContractFormula } from '../utils' export const info: ContractFormula = { compute: async ({ contractAddress, getTransformationMatch }) => { @@ -16,6 +17,12 @@ export const info: ContractFormula = { }, } +// cw-ownership +export const ownership = makeSimpleContractFormula({ + transformation: 'ownership', + fallbackKeys: ['ownership'], +}) + export const instantiatedAt: ContractFormula = { compute: async ({ contractAddress, getContract }) => { const timestamp = ( diff --git a/src/formulas/formulas/contract/external/cwVesting.ts b/src/formulas/formulas/contract/external/cwVesting.ts index 82db225f..47de671c 100644 --- a/src/formulas/formulas/contract/external/cwVesting.ts +++ b/src/formulas/formulas/contract/external/cwVesting.ts @@ -1,6 +1,11 @@ +import { Uint128 } from '@dao-dao/types' + import { ContractFormula } from '@/types' import { dbKeyToKeys } from '@/utils' +import { makeSimpleContractFormula } from '../../utils' +import { Vest } from './cwVesting.types' + type ValidatorStake = { validator: string timeMs: number @@ -8,16 +13,131 @@ type ValidatorStake = { amount: string } -export const info: ContractFormula = { - compute: async ({ contractAddress, get }) => - await get(contractAddress, 'vesting'), -} +export { ownership } from '../common' + +export const info: ContractFormula = makeSimpleContractFormula({ + transformation: 'vesting', + fallbackKeys: ['vesting'], +}) -export const unbondingDurationSeconds: ContractFormula = { - compute: async ({ contractAddress, get }) => - await get(contractAddress, 'ubs'), +export const vested: ContractFormula = makeSimpleContractFormula< + Vest, + Uint128, + { + /** + * Nanosecond timestamp. + */ + t: string + } +>({ + transformation: 'vesting', + fallbackKeys: ['vesting'], + transform: ({ vested, start_time }, { args: { t }, block }) => { + if (t && isNaN(Number(t))) { + throw new Error('Invalid timestamp (NaN).') + } + + // Convert millisecond block timestamp to nanoseconds. + const tNanos = t ? BigInt(t) : block.timeUnixMs * 1_000_000n + const elapsed = tNanos - BigInt(start_time) + + if ('constant' in vested) { + return vested.constant.y + } else if ('saturating_linear' in vested) { + const minX = BigInt(vested.saturating_linear.min_x) + const maxX = BigInt(vested.saturating_linear.max_x) + const minY = BigInt(vested.saturating_linear.min_y) + const maxY = BigInt(vested.saturating_linear.max_y) + return interpolate([minX, minY], [maxX, maxY], elapsed).toString() + } else if ('piecewise_linear' in vested) { + // figure out the pair of points it lies between + const steps = vested.piecewise_linear.steps + let [prev, next]: [[bigint, bigint] | undefined, [bigint, bigint]] = [ + undefined, + [BigInt(steps[0][0]), BigInt(steps[0][1])], + ] + for (const [stepX, stepY] of steps) { + // only break if x is not above prev + if (elapsed > next[0]) { + prev = next + next = [BigInt(stepX), BigInt(stepY)] + } else { + break + } + } + // at this time: + // prev may be None (this was lower than first point) + // x may equal prev.0 (use this value) + // x may be greater than next (if higher than last item) + // OR x may be between prev and next (interpolate) + if (prev) { + if (elapsed === prev[0]) { + // this handles exact match with low end + return prev[1].toString() + } else if (elapsed >= next[0]) { + // this handles both higher than all and exact match + return next[1].toString() + } else { + // here we do linear interpolation + return interpolate(prev, next, elapsed).toString() + } + } else { + // lower than all, use first + return next[1].toString() + } + } else { + throw new Error('Invalid curve') + } + }, +}) + +const interpolate = ( + [minX, minY]: [bigint, bigint], + [maxX, maxY]: [bigint, bigint], + x: bigint +): bigint => { + if (x < minX || maxX - minX === 0n) { + return minY + } else if (x > maxX) { + return maxY + } else if (maxY > minY) { + return minY + ((maxY - minY) * (x - minX)) / (maxX - minX) + } else { + // maxY <= minY + return minY - ((minY - maxY) * (x - minX)) / (maxX - minX) + } } +export const totalToVest: ContractFormula = makeSimpleContractFormula< + Vest, + Uint128 +>({ + transformation: 'vesting', + fallbackKeys: ['vesting'], + transform: ({ vested }) => { + if ('constant' in vested) { + return vested.constant.y + } else if ('saturating_linear' in vested) { + const minY = BigInt(vested.saturating_linear.min_y) + const maxY = BigInt(vested.saturating_linear.max_y) + return (maxY > minY ? maxY : minY).toString() + } else if ('piecewise_linear' in vested) { + const maxY = vested.piecewise_linear.steps + .map(([_, y]) => BigInt(y)) + .reduce((acc, y) => (y > acc ? y : acc), 0n) + return maxY.toString() + } else { + throw new Error('Invalid curve') + } + }, +}) + +export const unbondingDurationSeconds: ContractFormula = + makeSimpleContractFormula({ + transformation: 'ubs', + fallbackKeys: ['ubs'], + }) + // The amount staked and unstaking for each validator over time. export const validatorStakes: ContractFormula = { compute: async ({ contractAddress, getMap }) => { diff --git a/src/formulas/formulas/contract/external/cwVesting.types.ts b/src/formulas/formulas/contract/external/cwVesting.types.ts new file mode 100644 index 00000000..a36a4bce --- /dev/null +++ b/src/formulas/formulas/contract/external/cwVesting.types.ts @@ -0,0 +1,211 @@ +/** + * This file was automatically generated by @cosmwasm/ts-codegen@1.10.0. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +export type UncheckedDenom = + | { + native: string + } + | { + cw20: string + } +export type Schedule = + | 'saturating_linear' + | { + piecewise_linear: [number, Uint128][] + } +export type Uint128 = string +export type Timestamp = Uint64 +export type Uint64 = string +export interface InstantiateMsg { + denom: UncheckedDenom + description?: string | null + owner?: string | null + recipient: string + schedule: Schedule + start_time?: Timestamp | null + title: string + total: Uint128 + unbonding_duration_seconds: number + vesting_duration_seconds: number +} +export type ExecuteMsg = + | { + receive: Cw20ReceiveMsg + } + | { + distribute: { + amount?: Uint128 | null + } + } + | { + cancel: {} + } + | { + delegate: { + amount: Uint128 + validator: string + } + } + | { + redelegate: { + amount: Uint128 + dst_validator: string + src_validator: string + } + } + | { + undelegate: { + amount: Uint128 + validator: string + } + } + | { + set_withdraw_address: { + address: string + } + } + | { + withdraw_delegator_reward: { + validator: string + } + } + | { + withdraw_canceled_payment: { + amount?: Uint128 | null + } + } + | { + register_slash: { + amount: Uint128 + during_unbonding: boolean + time: Timestamp + validator: string + } + } + | { + update_ownership: Action + } +export type Binary = string +export type Action = + | { + transfer_ownership: { + expiry?: Expiration | null + new_owner: string + } + } + | 'accept_ownership' + | 'renounce_ownership' +export type Expiration = + | { + at_height: number + } + | { + at_time: Timestamp + } + | { + never: {} + } +export interface Cw20ReceiveMsg { + amount: Uint128 + msg: Binary + sender: string +} +export type QueryMsg = + | { + ownership: {} + } + | { + info: {} + } + | { + distributable: { + t?: Timestamp | null + } + } + | { + vested: { + t?: Timestamp | null + } + } + | { + total_to_vest: {} + } + | { + vest_duration: {} + } + | { + stake: StakeTrackerQuery + } +export type StakeTrackerQuery = + | { + cardinality: { + t: Timestamp + } + } + | { + total_staked: { + t: Timestamp + } + } + | { + validator_staked: { + t: Timestamp + validator: string + } + } +export type CheckedDenom = + | { + native: string + } + | { + cw20: Addr + } +export type Addr = string +export type Status = + | ('unfunded' | 'funded') + | { + canceled: { + owner_withdrawable: Uint128 + } + } +export type Curve = + | { + constant: { + y: Uint128 + } + } + | { + saturating_linear: SaturatingLinear + } + | { + piecewise_linear: PiecewiseLinear + } +export interface Vest { + claimed: Uint128 + denom: CheckedDenom + description?: string | null + recipient: Addr + slashed: Uint128 + start_time: Timestamp + status: Status + title: string + vested: Curve +} +export interface SaturatingLinear { + max_x: number + max_y: Uint128 + min_x: number + min_y: Uint128 +} +export interface PiecewiseLinear { + steps: [number, Uint128][] +} +export interface OwnershipForAddr { + owner?: Addr | null + pending_expiry?: Expiration | null + pending_owner?: Addr | null +} +export type NullableUint64 = Uint64 | null diff --git a/src/formulas/formulas/utils.ts b/src/formulas/formulas/utils.ts index 6c170a5f..66b34f34 100644 --- a/src/formulas/formulas/utils.ts +++ b/src/formulas/formulas/utils.ts @@ -1,4 +1,4 @@ -import { ContractFormula, Env, KeyInput } from '@/types' +import { Block, ContractFormula, Env, KeyInput } from '@/types' import { Duration, Expiration } from './types' @@ -44,7 +44,11 @@ export const expirationPlusDuration = ( /** * Make a simple contract formula with some common error/fallback handling. */ -export const makeSimpleContractFormula = ({ +export const makeSimpleContractFormula = < + T = unknown, + R = T, + Args extends Record = {} +>({ filter, fallback, transform = (data: T) => data as unknown as R, @@ -62,7 +66,8 @@ export const makeSimpleContractFormula = ({ */ transformation: string /** - * Fallback state key to load from WasmStateEvents table. + * Fallback state key(s) to load from WasmStateEvents table, fetched in + * order. */ fallbackKeys?: KeyInput[] } @@ -79,10 +84,22 @@ export const makeSimpleContractFormula = ({ /** * Optionally transform the data before returning it. */ - transform?: (data: T) => R -}): ContractFormula => ({ + transform?: ( + data: T, + options: { + args: Partial + block: Block + } + ) => R +}): ContractFormula => ({ filter, - compute: async ({ contractAddress, get, getTransformationMatch }) => { + compute: async ({ + contractAddress, + args, + block, + get, + getTransformationMatch, + }) => { const value = 'key' in source ? await get(contractAddress, ...[source.key].flat()) @@ -113,6 +130,9 @@ export const makeSimpleContractFormula = ({ throw new Error('failed to load') } - return transform(value) + return transform(value, { + args, + block, + }) }, }) diff --git a/src/transformers/transformers/common.ts b/src/transformers/transformers/common.ts index fcb13d4b..e826aad5 100644 --- a/src/transformers/transformers/common.ts +++ b/src/transformers/transformers/common.ts @@ -8,6 +8,8 @@ const info: Transformer = makeTransformer([], 'info', 'contract_info') // Transform for all contracts. cw-ownable ownership const KEY_OWNERSHIP = dbKeyForKeys('ownership') + +const ownership: Transformer = makeTransformer([], 'ownership') const owner: Transformer = { filter: { codeIdsKeys: [], @@ -17,4 +19,4 @@ const owner: Transformer = { getValue: (event) => event.valueJson.owner, } -export default [info, owner] +export default [info, ownership, owner] diff --git a/src/transformers/transformers/external/cwVesting.ts b/src/transformers/transformers/external/cwVesting.ts new file mode 100644 index 00000000..0af750a6 --- /dev/null +++ b/src/transformers/transformers/external/cwVesting.ts @@ -0,0 +1,10 @@ +import { Transformer } from '@/types' + +import { makeTransformer } from '../../utils' + +const CODE_IDS_KEYS: string[] = ['cw-vesting'] + +const vesting: Transformer = makeTransformer(CODE_IDS_KEYS, 'vesting') +const ubs: Transformer = makeTransformer(CODE_IDS_KEYS, 'ubs') + +export default [vesting, ubs] diff --git a/src/transformers/transformers/external/index.ts b/src/transformers/transformers/external/index.ts index 0c35ff9c..05619afe 100644 --- a/src/transformers/transformers/external/index.ts +++ b/src/transformers/transformers/external/index.ts @@ -2,5 +2,6 @@ import cw1Whitelist from './cw1Whitelist' import cw20 from './cw20' import cw4Group from './cw4Group' import cw721 from './cw721' +import cwVesting from './cwVesting' -export default [...cw1Whitelist, ...cw20, ...cw4Group, ...cw721] +export default [...cw1Whitelist, ...cw20, ...cw4Group, ...cw721, ...cwVesting]