UNPKG

@f5i23q999d/cow-sdk

Version:

<p align="center"> <img width="400" src="https://github.com/cowprotocol/cow-sdk/raw/main/docs/images/CoW.png" /> </p>

377 lines 16.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Multiplexer = void 0; const merkle_tree_1 = require("@openzeppelin/merkle-tree"); const ethers_1 = require("ethers"); const types_1 = require("./types.js"); const contracts_1 = require("./contracts.js"); const CONDITIONAL_ORDER_LEAF_ABI = ['address', 'bytes32', 'bytes']; const PAYLOAD_EMITTED_ABI = ['tuple(bytes32[] proof, tuple(address handler, bytes32 salt, bytes staticInput) params)[]']; /** * Multiplexer for conditional orders - using `ComposableCoW`! * * This class provides functionality to: * - Generate a merkle tree of conditional orders * - Generate proofs for all orders in the merkle tree * - Save proofs, with the ability to omit / skip specific conditional orders * - Support for passing an optional upload function to upload the proofs to a decentralized storage network */ class Multiplexer { static orderTypeRegistry = {}; chain; location; orders = {}; tree; ctx; /** * @param chain The `chainId` for where we're using `ComposableCoW`. * @param orders An optional array of conditional orders to initialize the merkle tree with. * @param root An optional root to verify against. * @param location The location of the proofs for the conditional orders. */ constructor(chain, orders, root, location = types_1.ProofLocation.PRIVATE) { this.chain = chain; this.location = location; // If orders are provided, the length must be > 0 if (orders && Object.keys(orders).length === 0) { throw new Error('orders must have non-zero length'); } // If orders are provided, so must a root, and vice versa if ((orders && !root) || (!orders && root)) { throw new Error('orders cannot have undefined root'); } // can only proceed past here if both orders and root are provided, or neither are // validate that no unknown order types are provided for (const orderKey in orders) { if (orders.hasOwnProperty(orderKey)) { const order = orders[orderKey]; if (!Multiplexer.orderTypeRegistry.hasOwnProperty(order.orderType)) { throw new Error(`Unknown order type: ${order.orderType}`); } } } // If orders (and therefore the root) are provided, generate the merkle tree if (orders) { this.orders = orders; // if generate was successful, we can verify the root if (this.getOrGenerateTree().root !== root) { throw new Error('root mismatch'); } } } // --- user facing serialization methods --- /** * Given a serialized multiplexer, create the multiplexer and rehydrate all conditional orders. * Integrity of the multiplexer will be verified by generating the merkle tree and verifying * the root. * * **NOTE**: Before using this method, you must register all conditional order types using `Multiplexer.registerOrderType`. * @param s The serialized multiplexer. * @returns The multiplexer with all conditional orders rehydrated. * @throws If the multiplexer cannot be deserialized. * @throws If the merkle tree cannot be generated. * @throws If the merkle tree cannot be verified against the root. */ static fromJSON(s) { // reviver function to deserialize the orders const reviver = (k, v) => { if (k === 'orders' && typeof v === 'object' && v !== null) { const orders = {}; for (const orderKey in v) { if (v.hasOwnProperty(orderKey)) { const { orderType, ...orderData } = v[orderKey]; if (Multiplexer.orderTypeRegistry.hasOwnProperty(orderType)) { const OrderConstructor = Multiplexer.orderTypeRegistry[orderType]; orders[orderKey] = new OrderConstructor(orderData); } else { throw new Error(`Unknown order type: ${orderType}`); } } } return orders; } // Make sure we deserialize `BigNumber` correctly if (typeof v === 'object' && v !== null && v.hasOwnProperty('type') && v.hasOwnProperty('hex')) { if (v.type === 'BigNumber') { return ethers_1.BigNumber.from(v); } } return v; }; const { chain, orders, root, location } = JSON.parse(s, reviver); const m = new Multiplexer(chain, orders, root); m.location = location; return m; } /** * Serialize the multiplexer to JSON. * * This will include all state necessary to reconstruct the multiplexer, including the root. * @remarks This will **NOT** include the merkle tree. * @returns The JSON representation of the multiplexer, including the root but excluding the merkle tree. */ toJSON() { const root = this.getOrGenerateTree().root; // serialize the multiplexer, including the root but excluding the merkle tree. return JSON.stringify({ ...this, root }, (k, v) => { // filter out the merkle tree if (k === 'tree') return undefined; if (typeof v === 'object' && v !== null && 'orderType' in v) { const conditionalOrder = v; return { ...conditionalOrder, orderType: conditionalOrder.orderType, }; } // We do not do any custom serialization of `BigNumber` in order to preserve it's type. return v; }); } // --- crud methods --- /** * Add a conditional order to the merkle tree. * @param order The order to add to the merkle tree. */ add(order) { order.assertIsValid(); this.orders[order.id] = order; this.reset(); } /** * Remove a conditional order from the merkle tree. * @param id The id of the `ConditionalOrder` to remove from the merkle tree. */ remove(id) { delete this.orders[id]; this.reset(); } /** * Update a given conditional order in the merkle tree. * @param id The id of the `ConditionalOrder` to update. * @param updater A function that takes the existing `ConditionalOrder` and context, returning an updated `ConditionalOrder`. */ update(id, updater) { // copy the existing order and update it, given the existing context (if any) const order = updater(this.orders[id], this.ctx); // delete the existing order delete this.orders[id]; // add the updated order this.orders[order.id] = order; this.reset(); } // --- accessors --- /** * Accessor for a given conditional order in the multiplexer. * @param id The `id` of the `ConditionalOrder` to retrieve. * @returns A `ConditionalOrder` with the given `id`. */ getById(id) { return this.orders[id]; } /** * Accessor for a given conditional order in the multiplexer. * @param i The index of the `ConditionalOrder` to retrieve. * @returns A `ConditionalOrder` at the given index. */ getByIndex(i) { return this.orders[this.orderIds[i]]; } /** * Get all the conditional order ids in the multiplexer. */ get orderIds() { return Object.keys(this.orders); } get root() { return this.getOrGenerateTree().root; } /** * Retrieve the merkle tree of orders, or generate it if it doesn't exist. * * **CAUTION**: Developers of the SDK should prefer to use this method instead of generating the * merkle tree themselves. This method makes use of caching to avoid generating the * merkle tree needlessly. * @throws If the merkle tree cannot be generated. * @returns The merkle tree for the current set of conditional orders. */ getOrGenerateTree() { if (!this.tree) { this.tree = merkle_tree_1.StandardMerkleTree.of(Object.values(this.orders).map((order) => [...Object.values(order.leaf)]), CONDITIONAL_ORDER_LEAF_ABI); } return this.tree; } // --- serialization for watchtowers / indexers --- /** * The primary method for watch towers to use when deserializing the proofs and parameters for the conditional orders. * @param s The serialized proofs with parameters for consumption by watchtowers / indexers. * @returns The `ProofWithParams` array. * @throws If the `ProofWithParams` array cannot be deserialized. */ static decodeFromJSON(s) { // no need to rehydrate `BigNumber` as this is fully ABI encoded return JSON.parse(s); } /** * The primary entry point for dapps integrating with `ComposableCoW` to generate the proofs and * parameters for the conditional orders. * * After populating the multiplexer with conditional orders, this method can be used to generate * the proofs and parameters for the conditional orders. The returned `ProofStruct` can then be * used with `setRoot` or `setRootWithContext` on a `ComposableCoW`-enabled Safe. * * @param filter {@link getProofs} * @parma locFn A function that takes the off-chain encoded input, and returns the `location` * for the `ProofStruct`, and the `data` for the `ProofStruct`. * @returns The ABI-encoded `ProofStruct` for `setRoot` and `setRootWithContext`. */ async prepareProofStruct(location = this.location, filter, uploader) { const data = async () => { switch (location) { case types_1.ProofLocation.PRIVATE: return '0x'; case types_1.ProofLocation.EMITTED: return this.encodeToABI(filter); case types_1.ProofLocation.SWARM: case types_1.ProofLocation.WAKU: case types_1.ProofLocation.IPFS: if (!uploader) throw new Error('Must provide an uploader function'); try { return await uploader(this.encodeToJSON(filter)); } catch (e) { throw new Error(`Error uploading to decentralized storage ${location}: ${e}`); } default: throw new Error('Unsupported location'); } }; return await data() .then((d) => { try { // validate that `d` is a valid `bytes` ready to be abi-encoded ethers_1.utils.hexlify(ethers_1.utils.arrayify(d)); // if we get here, we have a valid `data` field for the `ProofStruct` // This means that if there was an upload function, it was called and the upload was successful // note: we don't check if the location has changed because we don't care this.location = location; return { location, data: d, }; } catch (e) { throw new Error(`data returned by uploader is invalid`); } }) .catch((e) => { throw new Error(`Error preparing proof struct: ${e}`); }); } /** * Poll a conditional order to see if it is tradeable. * @param owner The owner of the conditional order. * @param p The proof and parameters. * @param chain Which chain to use for the ComposableCoW contract. * @param provider An RPC provider for the chain. * @param offChainInputFn A function, if provided, that will return the off-chain input for the conditional order. * @throws If the conditional order is not tradeable. * @returns The tradeable `GPv2Order.Data` struct and the `signature` for the conditional order. */ static async poll(owner, p, chain, provider, offChainInputFn) { const composableCow = (0, contracts_1.getComposableCow)(chain, provider); const offChainInput = offChainInputFn ? await offChainInputFn(owner, p.params) : '0x'; return await composableCow.getTradeableOrderWithSignature(owner, p.params, offChainInput, p.proof); } /** * The primary entry point for dumping the proofs and parameters for the conditional orders. * * This is to be used by watchtowers / indexers to store the proofs and parameters for the * conditional orders off-chain. The encoding returned by this method may **NOT** contain all * proofs and parameters, depending on the `filter` provided, and therefore should not be used * to rehydrate the multiplexer from a user's perspective. * @param filter {@link getProofs} * @returns A JSON-encoded string of the proofs and parameters for the conditional orders. */ dumpProofs(filter) { return this.encodeToJSON(filter); } dumpProofsAndParams(filter) { return this.getProofs(filter); } /** * Get the proofs with parameters for the conditional orders in the merkle tree. * @param filter A function that takes a conditional order and returns a boolean indicating * whether the order should be included in the proof. * @returns An array of proofs and their order's parameters for the conditional orders in the * merkle tree. */ getProofs(filter) { // Get a list of all entry indices in the tree, excluding any that don't match the filter return [...this.getOrGenerateTree().entries()] .map(([i, v]) => { if ((filter && filter(v)) || filter === undefined) { return { idx: i, value: v }; } else { return undefined; } }) .reduce((acc, x) => { if (x) { const p = { handler: x.value[0], salt: x.value[1], staticInput: x.value[2], }; acc.push({ proof: this.getOrGenerateTree().getProof(x.idx), params: p, }); } return acc; }, []); } /** * ABI-encode the proofs and parameters for the conditional orders in the merkle tree. * @param filter {@link getProofs} * @returns ABI-encoded `data` for the `ProofStruct`. */ encodeToABI(filter) { return ethers_1.utils.defaultAbiCoder.encode(PAYLOAD_EMITTED_ABI, [this.getProofs(filter)]); } /** * JSON-encode the proofs and parameters for the conditional orders in the merkle tree. * @param filter {@link getProofs} * @returns The JSON-encoded data for storage off-chain. */ encodeToJSON(filter) { return JSON.stringify(this.getProofs(filter)); } /** * A helper to reset the merkle tree. */ reset() { this.tree = undefined; } /** * Register a conditional order type with the multiplexer. * * **CAUTION**: This is required for using `Multiplexer.fromJSON` and `Multiplexer.toJSON`. * @param orderType The order type to register. * @param conditionalOrderClass The class to use for the given order type. */ static registerOrderType(orderType, conditionalOrderClass) { Multiplexer.orderTypeRegistry[orderType] = conditionalOrderClass; } /** * Reset the order type registry. */ static resetOrderTypeRegistry() { Multiplexer.orderTypeRegistry = {}; } } exports.Multiplexer = Multiplexer; //# sourceMappingURL=Multiplexer.js.map