From 433ded253ec8f9cf01caefa31f2b9c4a5e82ab39 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 3 Jul 2024 00:58:59 -0400 Subject: [PATCH] feat(undelegate): use Timestamp instead of Date - removes dateInSeconds, as cosmic-proto now returns { seconds: bigint; nanos: number } - nanoseconds are ignored for the wakeAt() calculation, as these seem immaterial - adds undelegate() tests for LOA and COA that advance timer service to verify behavior - updates LOA to return undefined instead of a TimestampRecord on .delegate() --- .../test/bootstrapTests/orchestration.test.ts | 5 ++++ packages/orchestration/src/cosmos-api.ts | 2 +- .../src/exos/cosmos-orchestration-account.js | 6 ++-- .../src/exos/local-orchestration-account.js | 12 +++++--- packages/orchestration/src/typeGuards.js | 6 ++++ packages/orchestration/src/utils/time.js | 9 ------ .../test/examples/stake-ica.contract.test.ts | 30 +++++++++++++------ .../local-orchestration-account-kit.test.ts | 19 ++++++++++-- packages/orchestration/test/ibc-mocks.ts | 13 +++++--- .../orchestration/test/staking-ops.test.ts | 13 +++++--- .../orchestration/test/utils/time.test.ts | 9 ------ packages/vats/tools/fake-bridge.js | 3 +- 12 files changed, 79 insertions(+), 48 deletions(-) diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index d4488b3e5eff..ef28ceb1aff6 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -191,6 +191,11 @@ test.serial('stakeAtom - smart wallet', async t => { ); }); +test.todo('undelegate wallet offer'); +test.todo('undelegate with multiple undelegations wallet offer'); +test.todo('redelegate wallet offer'); +test.todo('withdraw reward wallet offer'); + // XXX rely on .serial to be in sequence, and keep this one last test.serial('revise chain info', async t => { const { diff --git a/packages/orchestration/src/cosmos-api.ts b/packages/orchestration/src/cosmos-api.ts index 588aa73115a4..09f898087d23 100644 --- a/packages/orchestration/src/cosmos-api.ts +++ b/packages/orchestration/src/cosmos-api.ts @@ -11,7 +11,7 @@ import type { Order, } from '@agoric/cosmic-proto/ibc/core/channel/v1/channel.js'; import type { State as IBCConnectionState } from '@agoric/cosmic-proto/ibc/core/connection/v1/connection.js'; -import type { Brand, Purse } from '@agoric/ertp/src/types.js'; +import type { Brand, Purse, Payment, Amount } from '@agoric/ertp/src/types.js'; import type { Port } from '@agoric/network'; import { IBCChannelID, type IBCConnectionID } from '@agoric/vats'; import type { diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 2ab41bdf6461..d04c88074e05 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -32,7 +32,6 @@ import { } from '../typeGuards.js'; import { maxClockSkew, tryDecodeResponse } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; -import { dateInSeconds } from '../utils/time.js'; /** * @import {AmountArg, IcaAccount, ChainAddress, CosmosValidatorAddress, ICQConnection, StakingAccountActions, DenomAmount, OrchestrationAccountI, DenomArg} from '../types.js'; @@ -230,9 +229,8 @@ export const prepareCosmosOrchestrationAccountKit = ( const { completionTime } = response; completionTime || Fail`No completion time result ${result}`; return watch( - E(this.state.timer).wakeAt( - dateInSeconds(completionTime) + maxClockSkew, - ), + // ignore nanoseconds and just use seconds from Timestamp + E(this.state.timer).wakeAt(completionTime.seconds + maxClockSkew), ); }, }, diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 14baf5d73c95..443f6010535d 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -13,10 +13,11 @@ import { DenomAmountShape, DenomShape, IBCTransferOptionsShape, + TimestampProtoShape, } from '../typeGuards.js'; import { maxClockSkew } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; -import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; +import { makeTimestampHelper } from '../utils/time.js'; /** * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; @@ -87,7 +88,9 @@ export const prepareLocalOrchestrationAccountKit = ( { holder: HolderI, undelegateWatcher: M.interface('undelegateWatcher', { - onFulfilled: M.call([M.splitRecord({ completionTime: M.string() })]) + onFulfilled: M.call([ + M.splitRecord({ completionTime: TimestampProtoShape }), + ]) .optional(M.arrayOf(M.undefined())) // empty context .returns(VowShape), }), @@ -186,9 +189,10 @@ export const prepareLocalOrchestrationAccountKit = ( const { completionTime } = response[0]; return watch( E(timerService).wakeAt( - // TODO clean up date handling once we have real data - dateInSeconds(new Date(completionTime)) + maxClockSkew, + // ignore nanoseconds and just use seconds from Timestamp + BigInt(completionTime.seconds) + maxClockSkew, ), + this.facets.returnVoidWatcher, ); }, }, diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index e01063c7bb65..b65cdbdcb291 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -102,3 +102,9 @@ export const ChainFacadeI = M.interface('ChainFacade', { getChainInfo: M.call().returns(VowShape), makeAccount: M.call().returns(VowShape), }); + +/** + * for google/protobuf/timestamp.proto, not to be confused with TimestampShape + * from `@agoric/time` + */ +export const TimestampProtoShape = { seconds: M.nat(), nanos: M.number() }; diff --git a/packages/orchestration/src/utils/time.js b/packages/orchestration/src/utils/time.js index 18d31b142e32..2ddfcbc9cb82 100644 --- a/packages/orchestration/src/utils/time.js +++ b/packages/orchestration/src/utils/time.js @@ -53,12 +53,3 @@ export function makeTimestampHelper(timer) { } /** @typedef {Awaited>} TimestampHelper */ - -/** - * Convert a Date from a Cosmos message, which has millisecond precision, to a - * BigInt for number of seconds since epoch, for use in a timer. - * - * @param {Date} date - * @returns {bigint} - */ -export const dateInSeconds = date => BigInt(Math.floor(date.getTime() / 1000)); diff --git a/packages/orchestration/test/examples/stake-ica.contract.test.ts b/packages/orchestration/test/examples/stake-ica.contract.test.ts index 06f80ea98582..5326f27ec4c6 100644 --- a/packages/orchestration/test/examples/stake-ica.contract.test.ts +++ b/packages/orchestration/test/examples/stake-ica.contract.test.ts @@ -9,6 +9,7 @@ import { QueryBalanceRequest, QueryBalanceResponse, } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; +import { TimeMath } from '@agoric/time'; import { commonSetup } from '../supports.js'; import { type StakeIcaTerms } from '../../src/examples/stakeIca.contract.js'; import fetchedChainInfo from '../../src/fetched-chain-info.js'; @@ -22,6 +23,8 @@ import { ChainAddress, DenomAmount, } from '../../src/orchestration-api.js'; +import { maxClockSkew } from '../../src/utils/cosmos.js'; +import { UNBOND_PERIOD_SECONDS } from '../ibc-mocks.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); @@ -139,6 +142,7 @@ test('makeAccount, getAddress, getBalances, getBalance', async t => { test('delegate, undelegate, redelegate, withdrawReward', async t => { const { bootstrap } = await commonSetup(t); + const { timer } = bootstrap; const { publicFacet } = await startContract(bootstrap); const account = await E(publicFacet).makeAccount(); @@ -155,17 +159,23 @@ test('delegate, undelegate, redelegate, withdrawReward', async t => { }); t.is(delegation, undefined, 'delegation returns void'); - // TODO, fixme! - await t.throwsAsync( - E(account).undelegate([ - { - shares: '10', - validatorAddress: validatorAddr.address, - }, - ]), + const undelegatationP = E(account).undelegate([ { - message: /bad response/, + shares: '10', + validatorAddress: validatorAddr.address, }, + ]); + timer.advanceTo( + TimeMath.coerceTimestampRecord( + Number(UNBOND_PERIOD_SECONDS + maxClockSkew), + timer.getTimerBrand(), + ), + 'advance timer to end of unbonding period', + ); + t.is( + await undelegatationP, + undefined, + 'undelegation returns void after completion_time', ); const redelgation = await E(account).redelegate( @@ -187,6 +197,8 @@ test('delegate, undelegate, redelegate, withdrawReward', async t => { ); }); +test.todo('undelegate multiple delegations'); + test('makeAccountInvitationMaker', async t => { const { bootstrap } = await commonSetup(t); const { publicFacet, zoe } = await startContract(bootstrap); diff --git a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts index 9a5533a02354..89fa71a63604 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -5,11 +5,14 @@ import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { heapVowE as E } from '@agoric/vow/vat.js'; import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { Far } from '@endo/far'; +import { TimeMath } from '@agoric/time'; import { prepareLocalOrchestrationAccountKit } from '../../src/exos/local-orchestration-account.js'; import { ChainAddress } from '../../src/orchestration-api.js'; import { makeChainHub } from '../../src/exos/chain-hub.js'; import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; import { commonSetup } from '../supports.js'; +import { UNBOND_PERIOD_SECONDS } from '../ibc-mocks.js'; +import { maxClockSkew } from '../../src/utils/cosmos.js'; test('deposit, withdraw', async t => { const { bootstrap, brands, utils } = await commonSetup(t); @@ -131,9 +134,19 @@ test('delegate, undelegate', async t => { // 2. there are no return values // 3. there are no side-effects such as assets being locked await E(account).delegate(validatorAddress, bld.units(999)); - // TODO get the timer to fire so that this promise resolves - void E(account).undelegate(validatorAddress, bld.units(999)); - t.pass(); + const undelegateP = E(account).undelegate(validatorAddress, bld.units(999)); + timer.advanceTo( + TimeMath.coerceTimestampRecord( + Number(UNBOND_PERIOD_SECONDS + maxClockSkew), + timer.getTimerBrand(), + ), + 'advance timer to end of unbonding period', + ); + t.is( + await undelegateP, + undefined, + 'undelegate returns void after completion_time', + ); }); test('transfer', async t => { diff --git a/packages/orchestration/test/ibc-mocks.ts b/packages/orchestration/test/ibc-mocks.ts index b001f3b3c31a..d2bcf6f11f23 100644 --- a/packages/orchestration/test/ibc-mocks.ts +++ b/packages/orchestration/test/ibc-mocks.ts @@ -14,6 +14,7 @@ import { MsgWithdrawDelegatorReward, MsgWithdrawDelegatorRewardResponse, } from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/tx.js'; +import type { Timestamp } from '@agoric/cosmic-proto/google/protobuf/timestamp.js'; import { buildMsgResponseString, buildQueryResponseString, @@ -21,7 +22,6 @@ import { buildTxPacketString, buildQueryPacketString, } from '../tools/ibc-mocks.js'; -import { MILLISECONDS_PER_SECOND } from '../src/utils/time.js'; /** * TODO: provide mappings to cosmos error codes (and module specific error codes) @@ -54,9 +54,14 @@ const redelegation = { export const UNBOND_PERIOD_SECONDS = 5n; -const getCompletionTime = () => { - // 5 seconds fron unix epoch - return new Date(0 + Number(UNBOND_PERIOD_SECONDS * MILLISECONDS_PER_SECOND)); +/** + * returns Timestamp record 5 seconds from unix epoch. + */ +const getCompletionTime = (): Timestamp => { + return { + seconds: UNBOND_PERIOD_SECONDS, + nanos: 0, + }; }; export const protoMsgMocks = { diff --git a/packages/orchestration/test/staking-ops.test.ts b/packages/orchestration/test/staking-ops.test.ts index 88e2ebf4591b..9878f1a90db5 100644 --- a/packages/orchestration/test/staking-ops.test.ts +++ b/packages/orchestration/test/staking-ops.test.ts @@ -22,9 +22,11 @@ import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { decodeBase64 } from '@endo/base64'; import { Far } from '@endo/far'; +import { Timestamp } from '@agoric/cosmic-proto/google/protobuf/timestamp.js'; import { prepareCosmosOrchestrationAccountKit } from '../src/exos/cosmos-orchestration-account.js'; import type { ChainAddress, IcaAccount, ICQConnection } from '../src/types.js'; import { encodeTxResponse } from '../src/utils/cosmos.js'; +import { MILLISECONDS_PER_SECOND } from '../src/utils/time.js'; const trivialDelegateResponse = encodeTxResponse( {}, @@ -77,6 +79,11 @@ const time = { new Date(Number(ts.absValue) * 1000).toISOString(), }; +const dateToTimestamp = (date: Date): Timestamp => ({ + seconds: BigInt(date.getTime()) / MILLISECONDS_PER_SECOND, + nanos: 0, +}); + const makeScenario = () => { const mockAccount = ( addr = 'agoric1234', @@ -92,7 +99,7 @@ const makeScenario = () => { '/cosmos.staking.v1beta1.MsgBeginRedelegate': _m => { const response = MsgBeginRedelegateResponse.fromPartial({ - completionTime: new Date('2025-12-17T03:24:00Z'), + completionTime: dateToTimestamp(new Date('2025-12-17T03:24:00Z')), }); return encodeTxResponse( response, @@ -119,7 +126,7 @@ const makeScenario = () => { '/cosmos.staking.v1beta1.MsgUndelegate': _m => { const { completionTime } = configStaking; const response = MsgUndelegateResponse.fromPartial({ - completionTime: new Date(completionTime), + completionTime: dateToTimestamp(new Date(completionTime)), }); return encodeTxResponse(response, MsgUndelegateResponse.toProtoMsg); }, @@ -453,6 +460,4 @@ test(`undelegate waits for unbonding period`, async t => { t.deepEqual(calls, [{ msgs: [msg] }]); }); -test.todo(`delegate; undelegate; collect rewards`); -test.todo('undelegate uses a timer: begin; how long? wait; resolve'); test.todo('undelegate is cancellable - cosmos cancelUnbonding'); diff --git a/packages/orchestration/test/utils/time.test.ts b/packages/orchestration/test/utils/time.test.ts index 01f496814c35..7c62d63541a4 100644 --- a/packages/orchestration/test/utils/time.test.ts +++ b/packages/orchestration/test/utils/time.test.ts @@ -2,7 +2,6 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { TimeMath } from '@agoric/time'; import { - dateInSeconds, makeTimestampHelper, NANOSECONDS_PER_SECOND, SECONDS_PER_MINUTE, @@ -39,11 +38,3 @@ test('makeTimestampHelper - getCurrentTimestamp', async t => { 'timestamp is 4 seconds since unix epoch, in nanoseconds', ); }); - -test('dateInSeconds', t => { - t.is(dateInSeconds(new Date(1)), 0n); - t.is(dateInSeconds(new Date(999)), 0n); - t.is(dateInSeconds(new Date(1000)), 1n); - - t.is(dateInSeconds(new Date('2025-12-17T12:23:45Z')), 1765974225n); -}); diff --git a/packages/vats/tools/fake-bridge.js b/packages/vats/tools/fake-bridge.js index 74feaa7d2c83..692748de30f0 100644 --- a/packages/vats/tools/fake-bridge.js +++ b/packages/vats/tools/fake-bridge.js @@ -200,7 +200,8 @@ export const makeFakeLocalchainBridge = (zone, onToBridge = () => {}) => { } case '/cosmos.staking.v1beta1.MsgUndelegate': { return /** @type {JsonSafe} */ ({ - completionTime: new Date().toJSON(), + // 5 seconds from unix epoch + completionTime: { seconds: 5n, nanos: 0 }, }); } // returns one empty object per message unless specified