Skip to content

Commit

Permalink
feat(undelegate): use Timestamp instead of Date
Browse files Browse the repository at this point in the history
- 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()
  • Loading branch information
0xpatrickdev committed Jul 3, 2024
1 parent dd5af94 commit 433ded2
Show file tree
Hide file tree
Showing 12 changed files with 79 additions and 48 deletions.
5 changes: 5 additions & 0 deletions packages/boot/test/bootstrapTests/orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
);
},
},
Expand Down
12 changes: 8 additions & 4 deletions packages/orchestration/src/exos/local-orchestration-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
}),
Expand Down Expand Up @@ -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,
);
},
},
Expand Down
6 changes: 6 additions & 0 deletions packages/orchestration/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() };
9 changes: 0 additions & 9 deletions packages/orchestration/src/utils/time.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,3 @@ export function makeTimestampHelper(timer) {
}

/** @typedef {Awaited<ReturnType<typeof makeTimestampHelper>>} 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));
30 changes: 21 additions & 9 deletions packages/orchestration/test/examples/stake-ica.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -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();

Expand All @@ -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(
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 => {
Expand Down
13 changes: 9 additions & 4 deletions packages/orchestration/test/ibc-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ 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,
buildMsgErrorString,
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)
Expand Down Expand Up @@ -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 = {
Expand Down
13 changes: 9 additions & 4 deletions packages/orchestration/test/staking-ops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{},
Expand Down Expand Up @@ -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',
Expand All @@ -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,
Expand All @@ -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);
},
Expand Down Expand Up @@ -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');
9 changes: 0 additions & 9 deletions packages/orchestration/test/utils/time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});
3 changes: 2 additions & 1 deletion packages/vats/tools/fake-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ export const makeFakeLocalchainBridge = (zone, onToBridge = () => {}) => {
}
case '/cosmos.staking.v1beta1.MsgUndelegate': {
return /** @type {JsonSafe<MsgUndelegateResponse>} */ ({
completionTime: new Date().toJSON(),
// 5 seconds from unix epoch
completionTime: { seconds: 5n, nanos: 0 },
});
}
// returns one empty object per message unless specified
Expand Down

0 comments on commit 433ded2

Please sign in to comment.