-
Notifications
You must be signed in to change notification settings - Fork 206
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3986 from Agoric/mfig-pegasus-simplify
Simplify Pegasus for POLA
- Loading branch information
Showing
11 changed files
with
830 additions
and
571 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// @ts-check | ||
import { details as X } from '@agoric/assert'; | ||
|
||
import { AmountMath } from '@agoric/ertp'; | ||
import { E } from '@agoric/eventual-send'; | ||
import { Far } from '@endo/marshal'; | ||
import { makeOncePromiseKit } from './once-promise-kit.js'; | ||
|
||
/** | ||
* Create or return an existing courier promise kit. | ||
* | ||
* @template K | ||
* @param {K} key | ||
* @param {LegacyMap<K, PromiseRecord<Courier>>} keyToCourierPK | ||
*/ | ||
export const getCourierPK = (key, keyToCourierPK) => { | ||
if (keyToCourierPK.has(key)) { | ||
return keyToCourierPK.get(key); | ||
} | ||
|
||
// This is the first packet for this denomination. | ||
// Create a new Courier promise kit for it. | ||
const courierPK = makeOncePromiseKit(X`${key} already pegged`); | ||
|
||
keyToCourierPK.init(key, courierPK); | ||
return courierPK; | ||
}; | ||
|
||
/** | ||
* Create the [send, receive] pair. | ||
* | ||
* @typedef {Object} CourierArgs | ||
* @property {ContractFacet} zcf | ||
* @property {ERef<BoardDepositFacet>} board | ||
* @property {ERef<NameHub>} namesByAddress | ||
* @property {Denom} remoteDenom | ||
* @property {Brand} localBrand | ||
* @property {(zcfSeat: ZCFSeat, amounts: AmountKeywordRecord) => void} retain | ||
* @property {(zcfSeat: ZCFSeat, amounts: AmountKeywordRecord) => void} redeem | ||
* @property {ERef<TransferProtocol>} transferProtocol | ||
* @param {ERef<Connection>} connection | ||
* @returns {(args: CourierArgs) => Courier} | ||
*/ | ||
export const makeCourierMaker = | ||
connection => | ||
({ | ||
zcf, | ||
board, | ||
namesByAddress, | ||
remoteDenom, | ||
localBrand, | ||
retain, | ||
redeem, | ||
transferProtocol, | ||
}) => { | ||
/** @type {Sender} */ | ||
const send = async (zcfSeat, depositAddress) => { | ||
const tryToSend = async () => { | ||
const amount = zcfSeat.getAmountAllocated('Transfer', localBrand); | ||
const transferPacket = await E(transferProtocol).makeTransferPacket({ | ||
value: amount.value, | ||
remoteDenom, | ||
depositAddress, | ||
}); | ||
|
||
// Retain the payment. We must not proceed on failure. | ||
retain(zcfSeat, { Transfer: amount }); | ||
|
||
// The payment is already escrowed, and proposed to retain, so try sending. | ||
return E(connection) | ||
.send(transferPacket) | ||
.then(ack => E(transferProtocol).assertTransferPacketAck(ack)) | ||
.then( | ||
_ => zcfSeat.exit(), | ||
reason => { | ||
// Return the payment to the seat, if possible. | ||
redeem(zcfSeat, { Transfer: amount }); | ||
throw reason; | ||
}, | ||
); | ||
}; | ||
|
||
// Reflect any error back to the seat. | ||
return tryToSend().catch(reason => { | ||
zcfSeat.fail(reason); | ||
}); | ||
}; | ||
|
||
/** @type {Receiver} */ | ||
const receive = async ({ value, depositAddress }) => { | ||
const localAmount = AmountMath.make(localBrand, value); | ||
|
||
// Look up the deposit facet for this board address, if there is one. | ||
/** @type {DepositFacet} */ | ||
const depositFacet = await E(board) | ||
.getValue(depositAddress) | ||
.catch(_ => E(namesByAddress).lookup(depositAddress, 'depositFacet')); | ||
|
||
const { userSeat, zcfSeat } = zcf.makeEmptySeatKit(); | ||
|
||
// Redeem the backing payment. | ||
try { | ||
redeem(zcfSeat, { Transfer: localAmount }); | ||
zcfSeat.exit(); | ||
} catch (e) { | ||
zcfSeat.fail(e); | ||
throw e; | ||
} | ||
|
||
// Once we've gotten to this point, their payment is committed and | ||
// won't be refunded on a failed receive. | ||
const payout = await E(userSeat).getPayout('Transfer'); | ||
|
||
// Send the payout promise to the deposit facet. | ||
// | ||
// We don't want to wait for the depositFacet to return, so that | ||
// it can't hang up (i.e. DoS) an ordered channel, which relies on | ||
// us returning promptly. | ||
E(depositFacet) | ||
.receive(payout) | ||
.catch(_ => {}); | ||
|
||
return E(transferProtocol).makeTransferPacketAck(true); | ||
}; | ||
|
||
return Far('courier', { send, receive }); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// @ts-check | ||
import { Nat } from '@agoric/nat'; | ||
import { Far } from '@endo/marshal'; | ||
import { assert, details as X } from '@agoric/assert'; | ||
|
||
/** | ||
* @typedef {Object} ICS20TransferPacket Packet shape defined at: | ||
* https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer#data-structures | ||
* @property {string} amount The extent of the amount | ||
* @property {Denom} denom The denomination of the amount | ||
* @property {string} [sender] The sender address | ||
* @property {DepositAddress} receiver The receiver deposit address | ||
*/ | ||
|
||
/** | ||
* @param {string} s | ||
*/ | ||
const safeJSONParseObject = s => { | ||
/** @type {unknown} */ | ||
let obj; | ||
try { | ||
obj = JSON.parse(s); | ||
} catch (e) { | ||
assert.note(e, X`${s} is not valid JSON`); | ||
throw e; | ||
} | ||
assert.typeof(obj, 'object', X`${s} is not a JSON object`); | ||
assert(obj !== null, X`${s} is null`); | ||
return obj; | ||
}; | ||
|
||
/** | ||
* Convert an inbound packet to a local amount. | ||
* | ||
* @param {Bytes} packet | ||
* @returns {Promise<PacketParts>} | ||
*/ | ||
export const parseICS20TransferPacket = async packet => { | ||
const ics20Packet = safeJSONParseObject(packet); | ||
const { amount, denom, receiver } = ics20Packet; | ||
|
||
assert.typeof(denom, 'string', X`Denom ${denom} must be a string`); | ||
assert.typeof(receiver, 'string', X`Receiver ${receiver} must be a string`); | ||
|
||
// amount is a string in JSON. | ||
assert.typeof(amount, 'string', X`Amount ${amount} must be a string`); | ||
const bigValue = BigInt(amount); | ||
|
||
// If we overflow, or don't have a non-negative integer, throw an exception! | ||
const value = Nat(bigValue); | ||
|
||
return harden({ | ||
depositAddress: receiver, | ||
remoteDenom: denom, | ||
value, | ||
}); | ||
}; | ||
|
||
/** | ||
* Convert the amount to a packet to send. PacketParts.value is limited to | ||
* fungible (bigint) amounts. | ||
* | ||
* @param {PacketParts} param0 | ||
* @returns {Promise<Bytes>} | ||
*/ | ||
export const makeICS20TransferPacket = async ({ | ||
value, | ||
remoteDenom, | ||
depositAddress, | ||
}) => { | ||
// We're using Nat as a dynamic check for overflow. | ||
// @ts-ignore - this causes errors on some versions of TS, but not others. | ||
const stringValue = String(Nat(value)); | ||
|
||
// Generate the ics20-1 packet. | ||
/** @type {ICS20TransferPacket} */ | ||
const ics20 = { | ||
amount: stringValue, | ||
denom: remoteDenom, | ||
receiver: depositAddress, | ||
}; | ||
|
||
return JSON.stringify(ics20); | ||
}; | ||
|
||
/** | ||
* Check the results of the transfer. | ||
* | ||
* @param {Bytes} ack | ||
* @returns {Promise<void>} | ||
*/ | ||
export const assertICS20TransferPacketAck = async ack => { | ||
const { success, error } = safeJSONParseObject(ack); | ||
assert(success, X`ICS20 transfer error ${error}`); | ||
}; | ||
|
||
/** | ||
* Create results of the transfer. Acknowledgement shape defined at: | ||
* https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer#data-structures | ||
* | ||
* @param {boolean} success | ||
* @param {any} error | ||
* @returns {Promise<Bytes>} | ||
*/ | ||
export const makeICS20TransferPacketAck = async (success, error) => { | ||
if (success) { | ||
const ack = { success: true }; | ||
return JSON.stringify(ack); | ||
} | ||
const nack = { success: false, error: `${error}` }; | ||
return JSON.stringify(nack); | ||
}; | ||
|
||
/** @type {TransferProtocol} */ | ||
export const ICS20TransferProtocol = Far('ics20-1 transfer protocol', { | ||
makeTransferPacket: makeICS20TransferPacket, | ||
assertTransferPacketAck: assertICS20TransferPacketAck, | ||
parseTransferPacket: parseICS20TransferPacket, | ||
makeTransferPacketAck: makeICS20TransferPacketAck, | ||
}); |
Oops, something went wrong.