Skip to content

Commit 347fd76

Browse files
committed
chore: implement parseInboundTransfer
Refs: #11374
1 parent 0386b3f commit 347fd76

File tree

4 files changed

+227
-2
lines changed

4 files changed

+227
-2
lines changed

packages/orchestration/src/cosmos-api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,15 @@ export interface LocalAccountMethods extends StakingAccountActions {
357357
* @param tap
358358
*/
359359
monitorTransfers: (tap: TargetApp) => Promise<TargetRegistration>;
360+
/**
361+
* Parse an incoming transfer message and return its details.
362+
*/
363+
parseInboundTransfer: (packet: Record<string, any>) => Promise<{
364+
amount: DenomAmount;
365+
fromAccount: AccountId;
366+
toAccount: AccountId;
367+
extra: Record<string, any>;
368+
}>;
360369
}
361370

362371
/**

packages/orchestration/src/exos/local-orchestration-account.js

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ import { TransferRouteShape } from './chain-hub.js';
2828
/**
2929
* @import {HostOf} from '@agoric/async-flow';
3030
* @import {LocalChain, LocalChainAccount} from '@agoric/vats/src/localchain.js';
31-
* @import {AmountArg, CosmosChainAddress, DenomAmount, IBCMsgTransferOptions, IBCConnectionInfo, OrchestrationAccountCommon, LocalAccountMethods, TransferRoute, AccountId, AccountIdArg} from '@agoric/orchestration';
32-
* @import {ContractMeta, Invitation, OfferHandler, ZCF, ZCFSeat} from '@agoric/zoe';
31+
* @import {AmountArg, CosmosChainAddress, DenomAmount, IBCMsgTransferOptions, OrchestrationAccountCommon, LocalAccountMethods, TransferRoute, AccountIdArg, Denom, Bech32Address} from '@agoric/orchestration';
32+
* @import {OfferHandler, ZCF, ZCFSeat} from '@agoric/zoe';
33+
* @import {IBCEvent} from '@agoric/vats';
34+
* @import {QueryDenomHashResponse} from '@agoric/cosmic-proto/ibc/applications/transfer/v1/query.js';
35+
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
3336
* @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'.
3437
* @import {Zone} from '@agoric/zone';
3538
* @import {Remote} from '@agoric/internal';
@@ -78,6 +81,14 @@ const HolderI = M.interface('holder', {
7881
.returns(EVow$(M.string())),
7982
matchFirstPacket: M.call(M.any()).returns(EVow$(M.any())),
8083
monitorTransfers: M.call(M.remotable('TargetApp')).returns(EVow$(M.any())),
84+
parseInboundTransfer: M.call(M.recordOf(M.string(), M.any())).returns(
85+
Vow$({
86+
amount: DenomAmountShape,
87+
fromAccount: M.string(),
88+
toAccount: M.string(),
89+
extra: M.recordOf(M.string(), M.any()),
90+
}),
91+
),
8192
});
8293

8394
/** @type {{ [name: string]: [description: string, valueShape: Matcher] }} */
@@ -167,6 +178,10 @@ export const prepareLocalOrchestrationAccountKit = (
167178
M.arrayOf(DenomAmountShape),
168179
),
169180
}),
181+
parseInboundTransferWatcher: M.interface('parseInboundTransferWatcher', {
182+
onFulfilled: M.call(M.record(), M.record()).returns(M.record()),
183+
onRejected: M.call(M.any(), M.record()).returns(M.record()),
184+
}),
170185
invitationMakers: M.interface('invitationMakers', {
171186
CloseAccount: M.call().returns(M.promise()),
172187
Delegate: M.call(M.string(), AmountShape).returns(M.promise()),
@@ -481,6 +496,40 @@ export const prepareLocalOrchestrationAccountKit = (
481496
return harden(balances.map(toDenomAmount));
482497
},
483498
},
499+
parseInboundTransferWatcher: {
500+
/**
501+
* @param {QueryDenomHashResponse} localDenomHash
502+
* @param {Awaited<
503+
* ReturnType<LocalAccountMethods['parseInboundTransfer']>
504+
* >} naiveResult
505+
*/
506+
onFulfilled(localDenomHash, naiveResult) {
507+
const localDenom = `ibc/${localDenomHash.hash}`;
508+
const { amount, ...rest } = naiveResult;
509+
const { denom: _, ...amountRest } = amount;
510+
return harden({
511+
...rest,
512+
amount: { ...amountRest, denom: localDenom },
513+
});
514+
},
515+
/**
516+
* @param {unknown} reason
517+
* @param {Awaited<
518+
* ReturnType<LocalAccountMethods['parseInboundTransfer']>
519+
* >} naiveResult
520+
*/
521+
onRejected(reason, naiveResult) {
522+
if (
523+
reason instanceof Error &&
524+
String(reason.message).includes('denomination trace not found')
525+
) {
526+
// Looks like the trace was not found; The naive result is good enough.
527+
return naiveResult;
528+
}
529+
// Propagate other errors upwards.
530+
throw reason;
531+
},
532+
},
484533
holder: {
485534
/** @type {HostOf<OrchestrationAccountCommon['asContinuingOffer']>} */
486535
asContinuingOffer() {
@@ -716,6 +765,76 @@ export const prepareLocalOrchestrationAccountKit = (
716765
return resultV;
717766
});
718767
},
768+
/**
769+
* @type {HostOf<LocalAccountMethods['parseInboundTransfer']>}
770+
*/
771+
parseInboundTransfer(record) {
772+
trace('parseInboundTransfer', record);
773+
return asVow(() => {
774+
const packet =
775+
/** @type {IBCEvent<'writeAcknowledgement'>['packet']} */ (
776+
record
777+
);
778+
779+
/** @type {FungibleTokenPacketData} */
780+
const ftPacketData = JSON.parse(atob(packet.data));
781+
const {
782+
denom: transferDenom,
783+
sender,
784+
receiver,
785+
amount,
786+
} = ftPacketData;
787+
788+
/**
789+
* @param {string} address
790+
*/
791+
const resolveBech32Address = address =>
792+
chainHub.resolveAccountId(/** @type {Bech32Address} */ (address));
793+
794+
/**
795+
* @param {Denom} localDenom
796+
*/
797+
const buildReturnValue = localDenom =>
798+
harden({
799+
amount: /** @type {DenomAmount} */ ({
800+
value: BigInt(amount),
801+
denom: localDenom,
802+
}),
803+
fromAccount: resolveBech32Address(sender),
804+
toAccount: resolveBech32Address(receiver),
805+
extra: {
806+
...ftPacketData,
807+
},
808+
});
809+
810+
/**
811+
* @type {Denom}
812+
*/
813+
let denomOrTrace;
814+
815+
const prefix = `${packet.source_port}/${packet.source_channel}/`;
816+
trace({ transferDenom, prefix });
817+
if (transferDenom.startsWith(prefix)) {
818+
// Unwind, which may end up as a local denom.
819+
denomOrTrace = transferDenom.slice(prefix.length);
820+
} else {
821+
// If the denom is not local, attach its source.
822+
denomOrTrace = `${packet.destination_port}/${packet.destination_channel}/${transferDenom}`;
823+
}
824+
825+
// Find the local denom hash for the transferDenom, if there is one.
826+
return watch(
827+
E(localchain).query(
828+
typedJson(
829+
'/ibc.applications.transfer.v1.QueryDenomHashRequest',
830+
{ trace: denomOrTrace },
831+
),
832+
),
833+
this.facets.parseInboundTransferWatcher,
834+
buildReturnValue(denomOrTrace),
835+
);
836+
});
837+
},
719838
/** @type {HostOf<OrchestrationAccountCommon['transferSteps']>} */
720839
transferSteps(amount, msg) {
721840
return asVow(() => {

packages/orchestration/test/exos/local-orchestration-account-kit.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
55
import type { TargetApp } from '@agoric/vats/src/bridge-target.js';
66
import {
77
LOCALCHAIN_QUERY_ALL_BALANCES_RESPONSE,
8+
LOCALCHAIN_QUERY_DENOM_HASH_DEFAULT_VALUE,
89
SIMULATED_ERRORS,
910
} from '@agoric/vats/tools/fake-bridge.js';
1011
import { heapVowE as VE } from '@agoric/vow/vat.js';
@@ -524,3 +525,82 @@ test('getBalances', async t => {
524525
);
525526
t.is(queryMessages.length, 1, 'getBalances sends query to cosmos golang');
526527
});
528+
529+
test('parseInboundTransfer', async t => {
530+
const common = await commonSetup(t);
531+
common.utils.populateChainHub();
532+
const makeTestLOAKit = prepareMakeTestLOAKit(t, common);
533+
const account = await makeTestLOAKit();
534+
535+
const { value: target } = await VE(account).getAddress();
536+
537+
/**
538+
* Here we assume an account on CosmosHub that has BLD (funded by an Agoric account)
539+
* sends some 'ibc/hash('transfer/channel-1/' + ubld)' back to Agoric. Where "transfer"
540+
* is source port and "channel-1" is source channel for CosmosHub.
541+
*
542+
* parseInboundTransfer should be able to handle incoming denom 'transfer/channel-1/ubld'
543+
* as a local denom.
544+
*
545+
* Here's an example IBC packet we scraped from multichain:
546+
* @example
547+
* { event: 'writeAcknowledgement',
548+
* packet: { data: 'eyJhbW91bnQiOiIxMjUiLCJkZW5vbSI6InRyYW5zZmVyL2NoYW5uZWwtMS91YmxkIiwicmVjZWl2ZXIiOiJhZ29yaWMxMHJjaHBjNmFyZjVlNHhmMDYzODh5M21kdzc2M2pnZTZobng0Y3hxcWptend3dzRmMmQ4cnBxZjk4YW54N21lYXZmc2h5cXBxNHo2c2VhIiwic2VuZGVyIjoiY29zbW9zMXJzamR2aHpydDl0NnQwZnl5cnVseGpneHFlcHNtdnJmeXltNWh6In0=', destination_channel: 'channel-1', destination_port: 'transfer', sequence: '1', source_channel: 'channel-1', source_port: 'transfer', timeout_height: { revision_height: '0', revision_number: '0' }, timeout_timestamp: '14933241973593604096' }, relayer: '', target: 'agoric1udw356v6nyhagnnjgakh0dgeyvaten2urqqfd3888254xn3ssyjsmlny72', type: 'VTRANSFER_IBC_EVENT' }
549+
* And packet.data decodes to: '{"amount":"125","denom":"transfer/channel-1/ubld","receiver":"agoric10rchpc6arf5e4xf06388y3mdw763jge6hnx4cxqqjmzwww4f2d8rpqf98anx7meavfshyqpq4z6sea","sender":"cosmos1rsjdvhzrt9t6t0fyyrulxjgxqepsmvrfyym5hz"}'
550+
*/
551+
const bldResult = await VE(account).parseInboundTransfer(
552+
buildVTransferEvent({
553+
receiver: target,
554+
denom: 'transfer/channel-1/ubld',
555+
amount: 33n,
556+
destinationChannel: 'channel-0',
557+
sequence: '6',
558+
sourceChannel: 'channel-1',
559+
}).packet,
560+
);
561+
562+
t.log(bldResult);
563+
t.like(bldResult, {
564+
amount: {
565+
denom: 'ubld',
566+
value: 33n,
567+
},
568+
extra: {
569+
denom: 'transfer/channel-1/ubld',
570+
amount: '33',
571+
},
572+
});
573+
574+
/**
575+
* And here we send uatom from CosmosHub to Agoric. The expectation is that parseInboundTransfer returns
576+
* a hash as amount.denom instead of 'transfer/channel-0/uatom'.
577+
*
578+
* Here's an example IBC packet we scraped from multichain:
579+
* @example
580+
* { event: 'writeAcknowledgement',
581+
* packet: { data: 'eyJhbW91bnQiOiI5OSIsImRlbm9tIjoidWF0b20iLCJyZWNlaXZlciI6ImFnb3JpYzEwcmNocDJreHhxMmVlcThrc2RjeDQ3bjI0bHZteWQ5am1zeXRmZGY2NWxncG5qN2dnbDd0cTZrbThhbng3bWVhdmZzaHlxcHEycmZ1YWQiLCJzZW5kZXIiOiJjb3Ntb3Mxbnhsd3h2OXN3NG55ejVkM2E0c2d0NTNlMHNxOHFzeGNsbHB1NmsifQ==', destination_channel: 'channel-0', destination_port: 'transfer', sequence: '1', source_channel: 'channel-1', source_port: 'transfer', timeout_height: { revision_height: '0', revision_number: '0' }, timeout_timestamp: '14933241973593604096' }, relayer: '', target: 'agoric14trrq9vusrmgxur2lf42lkdjxjedcz95k5a205qee0yy0l9sdtdstw3zja', type: 'VTRANSFER_IBC_EVENT' }
582+
* And packet.data decodes to: '{"amount":"99","denom":"uatom","receiver":"agoric10rchp2kxxq2eeq8ksdcx47n24lvmyd9jmsytfdf65lgpnj7ggl7tq6km8anx7meavfshyqpq2rfuad","sender":"cosmos1nxlwxv9sw4nyz5d3a4sgt53e0sq8qsxcllpu6k"}'
583+
*/
584+
const atomResult = await VE(account).parseInboundTransfer(
585+
buildVTransferEvent({
586+
receiver: target,
587+
denom: 'uatom',
588+
amount: 35n,
589+
destinationChannel: 'channel-0',
590+
sequence: '6',
591+
sourceChannel: 'channel-1',
592+
}).packet,
593+
);
594+
595+
t.log(atomResult);
596+
t.like(atomResult, {
597+
amount: {
598+
denom: `ibc/${LOCALCHAIN_QUERY_DENOM_HASH_DEFAULT_VALUE}`,
599+
value: 35n,
600+
},
601+
extra: {
602+
denom: 'uatom',
603+
amount: '35',
604+
},
605+
});
606+
});

packages/vats/tools/fake-bridge.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ export const LOCALCHAIN_QUERY_ALL_BALANCES_RESPONSE = [
251251
},
252252
];
253253

254+
export const LOCALCHAIN_QUERY_DENOM_HASH_DEFAULT_VALUE = 'fakeDenomHash';
255+
254256
/**
255257
* Used to mock responses from Cosmos Golang back to SwingSet for for
256258
* E(lca).query() and E(lca).queryMany().
@@ -292,6 +294,21 @@ export const fakeLocalChainBridgeQueryHandler = message => {
292294
},
293295
};
294296
}
297+
case '/ibc.applications.transfer.v1.QueryDenomHashRequest': {
298+
// native agoric assets cause this query throw
299+
if (message.trace === 'ubld') {
300+
throw new Error('ubld denomination trace not found');
301+
}
302+
303+
return {
304+
error: '',
305+
height: '1',
306+
reply: {
307+
'@type': '/ibc.applications.transfer.v1.QueryDenomHashResponse',
308+
hash: LOCALCHAIN_QUERY_DENOM_HASH_DEFAULT_VALUE,
309+
},
310+
};
311+
}
295312
// returns one empty object per message unless specified
296313
default:
297314
return {};

0 commit comments

Comments
 (0)