Skip to content

Commit

Permalink
feat(pegasus): implement correct result and denom trace handling
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Feb 20, 2022
1 parent ae98f61 commit aacf1c3
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 59 deletions.
44 changes: 44 additions & 0 deletions packages/pegasus/src/ibc-trace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @ts-check
import { Far } from '@endo/marshal';
import { assert, details as X } from '@agoric/assert';

import { parse } from '@agoric/swingset-vat/src/vats/network/multiaddr.js';

/**
* Return a source-prefixed version of the denomination, as specified in
* ICS20-1.
*
* @param {Address} addr
* @param {Denom} denom
*/
const sourcePrefixedDenom = (addr, denom) => {
const ma = parse(addr);

const ibcPort = ma.find(([protocol]) => protocol === 'ibc-port');
assert(ibcPort, X`${addr} does not contain an IBC port`);
const ibcChannel = ma.find(([protocol]) => protocol === 'ibc-channel');
assert(ibcChannel, X`${addr} does not contain an IBC channel`);

return `${ibcPort[1]}/${ibcChannel[1]}/${denom}`;
};

/** @type {DenomTransformer} */
const transformer = {
getDenomsForLocalPeg: async (denom, _localAddress, remoteAddress) => {
return {
sendDenom: denom,
receiveDenom: sourcePrefixedDenom(remoteAddress, denom),
};
},
getDenomsForRemotePeg: async (denom, localAddress, _remoteAddress) => {
return {
sendDenom: sourcePrefixedDenom(localAddress, denom),
receiveDenom: denom,
};
},
};

export const IBCSourceTraceDenomTransformer = Far(
'IBC source trace denom transformer',
transformer,
);
17 changes: 13 additions & 4 deletions packages/pegasus/src/ics20.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { assert, details as X } from '@agoric/assert';
* @property {DepositAddress} receiver The receiver deposit address
*/

// As specified in ICS20, the success result is a base64-encoded '\0x1' byte.
const ICS20_TRANSFER_SUCCESS_RESULT = 'AQ==';

// ibc-go as late as v3 requires the `sender` to be nonempty, but doesn't
// actually use it on the receiving side. We don't need it on the sending side,
// either, so we can just omit it.
Expand Down Expand Up @@ -96,8 +99,14 @@ export const makeICS20TransferPacket = async ({
* @returns {Promise<void>}
*/
export const assertICS20TransferPacketAck = async ack => {
const { success, error } = safeJSONParseObject(ack);
assert(success, X`ICS20 transfer error ${error}`);
const { result, error } = safeJSONParseObject(ack);
assert(error === undefined, X`ICS20 transfer error ${error}`);
assert(result !== undefined, X`ICS20 transfer missing result in ${ack}`);
if (result !== ICS20_TRANSFER_SUCCESS_RESULT) {
// We don't want to throw an error here, because we want only to be able to
// differentiate between a transfer that failed and a transfer that succeeded.
console.warn(`ICS20 transfer succeeded with unexpected result: ${result}`);
}
};

/**
Expand All @@ -110,10 +119,10 @@ export const assertICS20TransferPacketAck = async ack => {
*/
export const makeICS20TransferPacketAck = async (success, error) => {
if (success) {
const ack = { success: true };
const ack = { result: ICS20_TRANSFER_SUCCESS_RESULT };
return JSON.stringify(ack);
}
const nack = { success: false, error: `${error}` };
const nack = { error: `${error}` };
return JSON.stringify(nack);
};

Expand Down
116 changes: 76 additions & 40 deletions packages/pegasus/src/pegasus.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import '@agoric/swingset-vat/src/vats/network/types.js';
import '@agoric/zoe/exported.js';

import '../exported.js';
import { IBCSourceTraceDenomTransformer } from './ibc-trace.js';
import { ICS20TransferProtocol } from './ics20.js';
import { makeCourierMaker, getCourierPK } from './courier.js';

const DEFAULT_DENOM_TRANSFORMER = IBCSourceTraceDenomTransformer;
const DEFAULT_TRANSFER_PROTOCOL = ICS20TransferProtocol;

const TRANSFER_PROPOSAL_SHAPE = {
Expand All @@ -35,8 +37,8 @@ const makePegasus = (zcf, board, namesByAddress) => {
* @typedef {Object} LocalDenomState
* @property {Address} localAddr
* @property {Address} remoteAddr
* @property {LegacyMap<Denom, PromiseRecord<Courier>>} remoteDenomToCourierPK
* @property {IterationObserver<Denom>} remoteDenomPublication
* @property {LegacyMap<Denom, PromiseRecord<Courier>>} receiveDenomToCourierPK
* @property {IterationObserver<Denom>} receiveDenomPublication
* @property {Subscription<Denom>} remoteDenomSubscription
* @property {bigint} lastDenomNonce Distinguish Pegasus-created denom names
* that are sent and received from a remote connection
Expand Down Expand Up @@ -65,7 +67,8 @@ const makePegasus = (zcf, board, namesByAddress) => {
*
* @typedef {Object} PegasusDescriptor
* @property {Brand} localBrand
* @property {Denom} remoteDenom
* @property {Denom} receiveDenom
* @property {Denom} sendDenom
* @property {string} allegedName
*
* @param {LocalDenomState} state
Expand All @@ -81,8 +84,11 @@ const makePegasus = (zcf, board, namesByAddress) => {
getLocalBrand() {
return desc.localBrand;
},
getRemoteDenom() {
return desc.remoteDenom;
getReceiveDenom() {
return desc.receiveDenom;
},
getSendDenom() {
return desc.sendDenom;
},
});

Expand All @@ -95,12 +101,14 @@ const makePegasus = (zcf, board, namesByAddress) => {
* @param {ReturnType<typeof makeCourierMaker>} param0.makeCourier
* @param {LocalDenomState} param0.localDenomState
* @param {ERef<TransferProtocol>} param0.transferProtocol
* @param {ERef<DenomTransformer>} param0.denomTransformer
* @returns {PegasusConnectionActions}
*/
const makePegasusConnectionActions = ({
makeCourier,
localDenomState,
transferProtocol,
denomTransformer,
}) => {
let checkAbort = () => {};

Expand All @@ -109,19 +117,19 @@ const makePegasus = (zcf, board, namesByAddress) => {

/** @type {PegasusConnectionActions} */
const pegasusConnectionActions = Far('pegasusConnectionActions', {
async rejectStuckTransfers(remoteDenom) {
async rejectStuckTransfers(receiveDenom) {
checkAbort();
const { remoteDenomToCourierPK } = localDenomState;
const { receiveDenomToCourierPK } = localDenomState;

const { reject, promise } = remoteDenomToCourierPK.get(remoteDenom);
const { reject, promise } = receiveDenomToCourierPK.get(receiveDenom);
// If rejected, the rejection is returned to our caller, so we have
// handled it correctly and that flow doesn't need to trigger an
// additional UnhandledRejectionWarning in our vat.
promise.catch(() => {});
reject(assert.error(X`${remoteDenom} is temporarily unavailable`));
reject(assert.error(X`${receiveDenom} is temporarily unavailable`));

// Allow new transfers to be initiated.
remoteDenomToCourierPK.delete(remoteDenom);
receiveDenomToCourierPK.delete(receiveDenom);
},
async pegRemote(
allegedName,
Expand All @@ -130,7 +138,7 @@ const makePegasus = (zcf, board, namesByAddress) => {
displayInfo = undefined,
) {
checkAbort();
const { remoteDenomToCourierPK } = localDenomState;
const { receiveDenomToCourierPK } = localDenomState;

// Create the issuer for the local erights corresponding to the remote values.
const localKeyword = createLocalIssuerKeyword();
Expand All @@ -142,13 +150,22 @@ const makePegasus = (zcf, board, namesByAddress) => {
checkAbort();
const { brand: localBrand } = zcfMint.getIssuerRecord();

const { sendDenom, receiveDenom } = await E(
denomTransformer,
).getDenomsForRemotePeg(
remoteDenom,
localDenomState.localAddr,
localDenomState.remoteAddr,
);
checkAbort();

// Describe how to retain/redeem pegged shadow erights.
const courier = makeCourier({
zcf,
localBrand,
board,
namesByAddress,
remoteDenom,
remoteDenom: sendDenom,
retain: (zcfSeat, amounts) =>
zcfMint.burnLosses(harden(amounts), zcfSeat),
redeem: (zcfSeat, amounts) => {
Expand All @@ -157,13 +174,14 @@ const makePegasus = (zcf, board, namesByAddress) => {
transferProtocol,
});

const courierPK = getCourierPK(remoteDenom, remoteDenomToCourierPK);
const courierPK = getCourierPK(receiveDenom, receiveDenomToCourierPK);
courierPK.resolve(courier);

checkAbort();
const peg = makePeg(localDenomState, {
localBrand,
remoteDenom,
sendDenom,
receiveDenom,
allegedName,
});
pegs.add(peg);
Expand All @@ -187,6 +205,15 @@ const makePegasus = (zcf, board, namesByAddress) => {
);
checkAbort();

const { sendDenom, receiveDenom } = await E(
denomTransformer,
).getDenomsForLocalPeg(
remoteDenom,
localDenomState.localAddr,
localDenomState.remoteAddr,
);
checkAbort();

/**
* Transfer amount (of localBrand) from loser to winner seats.
*
Expand Down Expand Up @@ -214,7 +241,7 @@ const makePegasus = (zcf, board, namesByAddress) => {
zcf,
board,
namesByAddress,
remoteDenom,
remoteDenom: sendDenom,
localBrand,
retain: (transferSeat, amounts) =>
transferAmountFrom(
Expand All @@ -235,14 +262,15 @@ const makePegasus = (zcf, board, namesByAddress) => {
transferProtocol,
});

const { remoteDenomToCourierPK } = localDenomState;
const { receiveDenomToCourierPK } = localDenomState;

const courierPK = getCourierPK(remoteDenom, remoteDenomToCourierPK);
const courierPK = getCourierPK(receiveDenom, receiveDenomToCourierPK);
courierPK.resolve(courier);

const peg = makePeg(localDenomState, {
localBrand,
remoteDenom,
sendDenom,
receiveDenom,
allegedName,
});
pegs.add(peg);
Expand All @@ -266,9 +294,13 @@ const makePegasus = (zcf, board, namesByAddress) => {
* Return a handler that can be used with the Network API.
*
* @param {ERef<TransferProtocol>} [transferProtocol=DEFAULT_TRANSFER_PROTOCOL]
* @param {ERef<DenomTransformer>} [denomTransformer=DEFAULT_DENOM_TRANSFORMER]
* @returns {PegasusConnectionKit}
*/
makePegasusConnectionKit(transferProtocol = DEFAULT_TRANSFER_PROTOCOL) {
makePegasusConnectionKit(
transferProtocol = DEFAULT_TRANSFER_PROTOCOL,
denomTransformer = DEFAULT_DENOM_TRANSFORMER,
) {
/**
* @type {LegacyWeakMap<Connection, LocalDenomState>}
*/
Expand All @@ -289,17 +321,17 @@ const makePegasus = (zcf, board, namesByAddress) => {
// Register `c` with the table of Peg receivers.
const {
subscription: remoteDenomSubscription,
publication: remoteDenomPublication,
publication: receiveDenomPublication,
} = makeSubscriptionKit();
const remoteDenomToCourierPK = makeLegacyMap('Denomination');
const receiveDenomToCourierPK = makeLegacyMap('Denomination');

/** @type {LocalDenomState} */
const localDenomState = {
localAddr,
remoteAddr,
remoteDenomToCourierPK,
receiveDenomToCourierPK,
lastDenomNonce: 0n,
remoteDenomPublication,
receiveDenomPublication,
remoteDenomSubscription,
abort: reason => {
// eslint-disable-next-line no-use-before-define
Expand All @@ -313,6 +345,7 @@ const makePegasus = (zcf, board, namesByAddress) => {
localDenomState,
makeCourier,
transferProtocol,
denomTransformer,
});

connectionToLocalDenomState.init(c, localDenomState);
Expand All @@ -333,19 +366,22 @@ const makePegasus = (zcf, board, namesByAddress) => {
packetBytes,
);

const { remoteDenom } = parts;
assert.typeof(remoteDenom, 'string');
const { remoteDenom: receiveDenom } = parts;
assert.typeof(receiveDenom, 'string');

const { remoteDenomToCourierPK, remoteDenomPublication } =
const { receiveDenomToCourierPK, receiveDenomPublication } =
connectionToLocalDenomState.get(c);

if (!remoteDenomToCourierPK.has(remoteDenom)) {
if (!receiveDenomToCourierPK.has(receiveDenom)) {
// This is the first time we've heard of this denomination.
remoteDenomPublication.updateState(remoteDenom);
receiveDenomPublication.updateState(receiveDenom);
}

// Wait for the courier to be instantiated.
const courierPK = getCourierPK(remoteDenom, remoteDenomToCourierPK);
const courierPK = getCourierPK(
receiveDenom,
receiveDenomToCourierPK,
);
const { receive } = await courierPK.promise;
return receive(parts);
};
Expand All @@ -358,22 +394,22 @@ const makePegasus = (zcf, board, namesByAddress) => {
// Unregister `c`. Pending transfers will be rejected by the Network
// API.
const {
remoteDenomPublication,
remoteDenomToCourierPK,
receiveDenomPublication,
receiveDenomToCourierPK,
localAddr,
remoteAddr,
abort,
} = connectionToLocalDenomState.get(c);
connectionToLocalDenomState.delete(c);
const err = assert.error(X`pegasusConnectionHandler closed`);
remoteDenomPublication.fail(err);
receiveDenomPublication.fail(err);
/** @type {PegasusConnection} */
const state = harden({
localAddr,
remoteAddr,
});
connectionPublication.updateState(state);
for (const courierPK of remoteDenomToCourierPK.values()) {
for (const courierPK of receiveDenomToCourierPK.values()) {
try {
courierPK.reject(err);
} catch (e) {
Expand Down Expand Up @@ -407,10 +443,13 @@ const makePegasus = (zcf, board, namesByAddress) => {
const denomState = pegToDenomState.get(peg);

// Get details from the peg.
const remoteDenom = await E(peg).getRemoteDenom();
const { remoteDenomToCourierPK } = denomState;
const [receiveDenom, sendDenom] = await Promise.all([
E(peg).getReceiveDenom(),
E(peg).getSendDenom(),
]);
const { receiveDenomToCourierPK } = denomState;

const courierPK = getCourierPK(remoteDenom, remoteDenomToCourierPK);
const courierPK = getCourierPK(receiveDenom, receiveDenomToCourierPK);
const { send } = await courierPK.promise;

/**
Expand All @@ -423,10 +462,7 @@ const makePegasus = (zcf, board, namesByAddress) => {
send(zcfSeat, depositAddress);
};

return zcf.makeInvitation(
offerHandler,
`pegasus ${remoteDenom} transfer`,
);
return zcf.makeInvitation(offerHandler, `pegasus ${sendDenom} transfer`);
},
});
};
Expand Down
Loading

0 comments on commit aacf1c3

Please sign in to comment.