@dolomite-exchange/dolomite-margin
Version:
Ethereum Smart Contracts and TypeScript library used for the DolomiteMargin trading protocol
389 lines • 19 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SignedOperations = void 0;
const web3_1 = __importDefault(require("web3"));
const Signer_1 = require("./Signer");
const Constants_1 = require("../lib/Constants");
const Helpers_1 = require("../lib/Helpers");
const BytesHelper_1 = require("../lib/BytesHelper");
const SignatureHelper_1 = require("../lib/SignatureHelper");
const types_1 = require("../types");
const EIP712_OPERATION_STRUCT = [
{ type: 'Action[]', name: 'actions' },
{ type: 'uint256', name: 'expiration' },
{ type: 'uint256', name: 'salt' },
{ type: 'address', name: 'sender' },
{ type: 'address', name: 'signer' },
];
const EIP712_ACTION_STRUCT = [
{ type: 'uint8', name: 'actionType' },
{ type: 'address', name: 'accountOwner' },
{ type: 'uint256', name: 'accountNumber' },
{ type: 'AssetAmount', name: 'assetAmount' },
{ type: 'uint256', name: 'primaryMarketId' },
{ type: 'uint256', name: 'secondaryMarketId' },
{ type: 'address', name: 'otherAddress' },
{ type: 'address', name: 'otherAccountOwner' },
{ type: 'uint256', name: 'otherAccountNumber' },
{ type: 'bytes', name: 'data' },
];
const EIP712_ASSET_AMOUNT_STRUCT = [
{ type: 'bool', name: 'sign' },
{ type: 'uint8', name: 'denomination' },
{ type: 'uint8', name: 'ref' },
{ type: 'uint256', name: 'value' },
];
const EIP712_ASSET_AMOUNT_STRING = 'AssetAmount(' +
'bool sign,' +
'uint8 denomination,' +
'uint8 ref,' +
'uint256 value' +
')';
const EIP712_ACTION_STRING = 'Action(' + // tslint:disable-line
'uint8 actionType,' +
'address accountOwner,' +
'uint256 accountNumber,' +
'AssetAmount assetAmount,' +
'uint256 primaryMarketId,' +
'uint256 secondaryMarketId,' +
'address otherAddress,' +
'address otherAccountOwner,' +
'uint256 otherAccountNumber,' +
'bytes data' +
')' +
EIP712_ASSET_AMOUNT_STRING;
const EIP712_OPERATION_STRING = 'Operation(' + // tslint:disable-line
'Action[] actions,' +
'uint256 expiration,' +
'uint256 salt,' +
'address sender,' +
'address signer' +
')' +
EIP712_ACTION_STRING;
const EIP712_CANCEL_OPERATION_STRUCT = [
{ type: 'string', name: 'action' },
{ type: 'bytes32[]', name: 'operationHashes' },
];
const EIP712_CANCEL_OPERATION_STRUCT_STRING = 'CancelOperation(' + 'string action,' + 'bytes32[] operationHashes' + ')';
class SignedOperations extends Signer_1.Signer {
// ============ Constructor ============
constructor(contracts, web3, networkId) {
super(web3);
this.contracts = contracts;
this.networkId = networkId;
}
// ============ On-Chain Cancel ============
/**
* Sends an transaction to cancel an operation on-chain.
*/
async cancelOperation(operation, options) {
const accounts = [];
const actions = [];
const getAccountId = (accountOwner, accountNumber) => {
if (accountOwner === Constants_1.ADDRESSES.ZERO) {
return 0;
}
const accountInfo = {
owner: accountOwner,
number: accountNumber.toFixed(0),
};
const index = accounts.indexOf(accountInfo);
if (index >= 0) {
return index;
}
accounts.push(accountInfo);
return accounts.length - 1;
};
for (let i = 0; i < operation.actions.length; i += 1) {
const action = operation.actions[i];
actions.push({
accountId: getAccountId(action.primaryAccountOwner, action.primaryAccountNumber),
actionType: action.actionType,
primaryMarketId: Helpers_1.toString(action.primaryMarketId),
secondaryMarketId: Helpers_1.toString(action.secondaryMarketId),
otherAddress: action.otherAddress,
otherAccountId: getAccountId(action.secondaryAccountOwner, action.secondaryAccountNumber),
data: BytesHelper_1.hexStringToBytes(action.data),
amount: {
sign: action.amount.sign,
ref: Helpers_1.toString(action.amount.ref),
denomination: Helpers_1.toString(action.amount.denomination),
value: Helpers_1.toString(action.amount.value),
},
});
}
return this.contracts.callContractFunction(this.contracts.signedOperationProxy.methods.cancel(accounts, actions, {
numActions: operation.actions.length.toString(),
header: {
expiration: operation.expiration.toFixed(0),
salt: operation.salt.toFixed(0),
sender: operation.sender,
signer: operation.signer,
},
signature: [],
}), options || { from: operation.signer });
}
// ============ Getter Contract Methods ============
/**
* Returns true if the contract can process operations.
*/
async isOperational(options) {
return this.contracts.callConstantContractFunction(this.contracts.signedOperationProxy.methods.g_isOperational(), options);
}
/**
* Gets the status and the current filled amount (in makerAmount) of all given orders.
*/
async getOperationsAreInvalid(operations, options) {
const hashes = operations.map(operation => this.getOperationHash(operation));
return this.contracts.callConstantContractFunction(this.contracts.signedOperationProxy.methods.getOperationsAreInvalid(hashes), options);
}
// ============ Signing Methods ============
/**
* Sends operation to current provider for signing. Can sign locally if the signing account is
* loaded into web3 and SigningMethod.Hash is used.
*/
async signOperation(operation, signingMethod) {
switch (signingMethod) {
case types_1.SigningMethod.Hash:
case types_1.SigningMethod.UnsafeHash:
case types_1.SigningMethod.Compatibility:
const hash = this.getOperationHash(operation);
const rawSignature = await this.web3.eth.sign(hash, operation.signer);
const hashSig = SignatureHelper_1.createTypedSignature(rawSignature, SignatureHelper_1.SIGNATURE_TYPES.DECIMAL);
if (signingMethod === types_1.SigningMethod.Hash) {
return hashSig;
}
const unsafeHashSig = SignatureHelper_1.createTypedSignature(rawSignature, SignatureHelper_1.SIGNATURE_TYPES.NO_PREPEND);
if (signingMethod === types_1.SigningMethod.UnsafeHash) {
return unsafeHashSig;
}
if (this.operationByHashHasValidSignature(hash, unsafeHashSig, operation.signer)) {
return unsafeHashSig;
}
return hashSig;
case types_1.SigningMethod.TypedData:
case types_1.SigningMethod.MetaMask:
case types_1.SigningMethod.MetaMaskLatest:
case types_1.SigningMethod.CoinbaseWallet:
return this.ethSignTypedOperationInternal(operation, signingMethod);
default:
throw new Error(`Invalid signing method ${signingMethod}`);
}
}
/**
* Sends operation to current provider for signing of a cancel message. Can sign locally if the
* signing account is loaded into web3 and SigningMethod.Hash is used.
*/
async signCancelOperation(operation, signingMethod) {
return this.signCancelOperationByHash(this.getOperationHash(operation), operation.signer, signingMethod);
}
/**
* Sends operationHash to current provider for signing of a cancel message. Can sign locally if
* the signing account is loaded into web3 and SigningMethod.Hash is used.
*/
async signCancelOperationByHash(operationHash, signer, signingMethod) {
switch (signingMethod) {
case types_1.SigningMethod.Hash:
case types_1.SigningMethod.UnsafeHash:
case types_1.SigningMethod.Compatibility:
const cancelHash = this.operationHashToCancelOperationHash(operationHash);
const rawSignature = await this.web3.eth.sign(cancelHash, signer);
const hashSig = SignatureHelper_1.createTypedSignature(rawSignature, SignatureHelper_1.SIGNATURE_TYPES.DECIMAL);
if (signingMethod === types_1.SigningMethod.Hash) {
return hashSig;
}
const unsafeHashSig = SignatureHelper_1.createTypedSignature(rawSignature, SignatureHelper_1.SIGNATURE_TYPES.NO_PREPEND);
if (signingMethod === types_1.SigningMethod.UnsafeHash) {
return unsafeHashSig;
}
if (this.cancelOperationByHashHasValidSignature(operationHash, unsafeHashSig, signer)) {
return unsafeHashSig;
}
return hashSig;
case types_1.SigningMethod.TypedData:
case types_1.SigningMethod.MetaMask:
case types_1.SigningMethod.MetaMaskLatest:
case types_1.SigningMethod.CoinbaseWallet:
return this.ethSignTypedCancelOperationInternal(operationHash, signer, signingMethod);
default:
throw new Error(`Invalid signing method ${signingMethod}`);
}
}
// ============ Signing Cancel Operation Methods ============
// noinspection JSUnusedGlobalSymbols
/**
* Uses web3.eth.sign to sign a cancel message for an operation. This signature is not used
* on-chain,but allows backend services to verify that the cancel operation api call is from
* the original maker of the operation.
*/
async ethSignCancelOperation(operation) {
return this.ethSignCancelOperationByHash(this.getOperationHash(operation), operation.signer);
}
/**
* Uses web3.eth.sign to sign a cancel message for an operation hash. This signature is not used
* on-chain, but allows dYdX backend services to verify that the cancel operation api call is from
* the original maker of the operation.
*/
async ethSignCancelOperationByHash(operationHash, signer) {
const cancelHash = this.operationHashToCancelOperationHash(operationHash);
const signature = await this.web3.eth.sign(cancelHash, signer);
return SignatureHelper_1.createTypedSignature(signature, SignatureHelper_1.SIGNATURE_TYPES.DECIMAL);
}
// ============ Signature Verification ============
/**
* Returns true if the operation object has a non-null valid signature from the maker of the
* operation.
*/
operationHasValidSignature(signedOperation) {
return this.operationByHashHasValidSignature(this.getOperationHash(signedOperation), signedOperation.typedSignature, signedOperation.signer);
}
/**
* Returns true if the operation hash has a non-null valid signature from a particular signer.
*/
operationByHashHasValidSignature(operationHash, typedSignature, expectedSigner) {
const signer = SignatureHelper_1.ecRecoverTypedSignature(operationHash, typedSignature);
return BytesHelper_1.addressesAreEqual(signer, expectedSigner);
}
/**
* Returns true if the cancel operation message has a valid signature.
*/
cancelOperationHasValidSignature(operation, typedSignature) {
return this.cancelOperationByHashHasValidSignature(this.getOperationHash(operation), typedSignature, operation.signer);
}
/**
* Returns true if the cancel operation message has a valid signature.
*/
cancelOperationByHashHasValidSignature(operationHash, typedSignature, expectedSigner) {
const cancelHash = this.operationHashToCancelOperationHash(operationHash);
const signer = SignatureHelper_1.ecRecoverTypedSignature(cancelHash, typedSignature);
return BytesHelper_1.addressesAreEqual(signer, expectedSigner);
}
// ============ Hashing Functions ============
/**
* Returns the final signable EIP712 hash for approving an operation.
*/
getOperationHash(operation) {
const structHash = web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(EIP712_OPERATION_STRING) }, { t: 'bytes32', v: this.getActionsHash(operation.actions) }, { t: 'uint256', v: Helpers_1.toString(operation.expiration) }, { t: 'uint256', v: Helpers_1.toString(operation.salt) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(operation.sender) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(operation.signer) });
return this.getEIP712Hash(structHash);
}
/**
* Returns the EIP712 hash of the actions array.
*/
getActionsHash(actions) {
const actionsAsHashes = actions.length
? actions
.map(action => BytesHelper_1.stripHexPrefix(this.getActionHash(action)))
.join('')
: '';
return BytesHelper_1.hashBytes(actionsAsHashes);
}
/**
* Returns the EIP712 hash of a single Action struct.
*/
getActionHash(action) {
return web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(EIP712_ACTION_STRING) }, { t: 'uint256', v: Helpers_1.toString(action.actionType) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(action.primaryAccountOwner) }, { t: 'uint256', v: Helpers_1.toString(action.primaryAccountNumber) }, { t: 'bytes32', v: this.getAssetAmountHash(action.amount) }, { t: 'uint256', v: Helpers_1.toString(action.primaryMarketId) }, { t: 'uint256', v: Helpers_1.toString(action.secondaryMarketId) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(action.otherAddress) }, { t: 'bytes32', v: BytesHelper_1.addressToBytes32(action.secondaryAccountOwner) }, { t: 'uint256', v: Helpers_1.toString(action.secondaryAccountNumber) }, { t: 'bytes32', v: BytesHelper_1.hashBytes(action.data) });
}
/**
* Returns the EIP712 hash of an AssetAmount struct.
*/
getAssetAmountHash(amount) {
return web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(EIP712_ASSET_AMOUNT_STRING) }, { t: 'uint256', v: Helpers_1.toString(amount.sign ? 1 : 0) }, { t: 'uint256', v: Helpers_1.toString(amount.denomination) }, { t: 'uint256', v: Helpers_1.toString(amount.ref) }, { t: 'uint256', v: Helpers_1.toString(amount.value) });
}
/**
* Given some operation hash, returns the hash of a cancel-operation message.
*/
operationHashToCancelOperationHash(operationHash) {
const structHash = web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(EIP712_CANCEL_OPERATION_STRUCT_STRING) }, { t: 'bytes32', v: BytesHelper_1.hashString('Cancel Operations') }, {
t: 'bytes32',
v: web3_1.default.utils.soliditySha3({ t: 'bytes32', v: operationHash }),
});
return this.getEIP712Hash(structHash);
}
/**
* Returns the EIP712 domain separator hash.
*/
getDomainHash() {
return web3_1.default.utils.soliditySha3({ t: 'bytes32', v: BytesHelper_1.hashString(SignatureHelper_1.EIP712_DOMAIN_STRING) }, { t: 'bytes32', v: BytesHelper_1.hashString('SignedOperationProxy') }, { t: 'bytes32', v: BytesHelper_1.hashString('1.1') }, { t: 'uint256', v: Helpers_1.toString(this.networkId) }, {
t: 'bytes32',
v: BytesHelper_1.addressToBytes32(this.contracts.signedOperationProxy.options.address),
});
}
getNetworkDomainHash() {
return this.contracts.signedOperationProxy.methods.EIP712_DOMAIN_HASH().call();
}
/**
* Returns a signable EIP712 Hash of a struct
*/
getEIP712Hash(structHash) {
return web3_1.default.utils.soliditySha3({ t: 'bytes2', v: '0x1901' }, { t: 'bytes32', v: this.getDomainHash() }, { t: 'bytes32', v: structHash });
}
// ============ Private Helper Functions ============
getDomainData() {
return {
name: 'SignedOperationProxy',
version: '1.1',
chainId: this.networkId,
verifyingContract: this.contracts.signedOperationProxy.options.address,
};
}
async ethSignTypedOperationInternal(operation, signingMethod) {
const actionsData = operation.actions.map((action) => {
return {
actionType: Helpers_1.toString(action.actionType),
accountOwner: action.primaryAccountOwner,
accountNumber: Helpers_1.toString(action.primaryAccountNumber),
assetAmount: {
sign: action.amount.sign,
denomination: Helpers_1.toString(action.amount.denomination),
ref: Helpers_1.toString(action.amount.ref),
value: Helpers_1.toString(action.amount.value),
},
primaryMarketId: Helpers_1.toString(action.primaryMarketId),
secondaryMarketId: Helpers_1.toString(action.secondaryMarketId),
otherAddress: action.otherAddress,
otherAccountOwner: action.secondaryAccountOwner,
otherAccountNumber: Helpers_1.toString(action.secondaryAccountNumber),
data: action.data,
};
});
const operationData = {
actions: actionsData,
expiration: operation.expiration.toFixed(0),
salt: operation.salt.toFixed(0),
sender: operation.sender,
signer: operation.signer,
};
const data = {
types: {
EIP712Domain: SignatureHelper_1.EIP712_DOMAIN_STRUCT,
Operation: EIP712_OPERATION_STRUCT,
Action: EIP712_ACTION_STRUCT,
AssetAmount: EIP712_ASSET_AMOUNT_STRUCT,
},
domain: this.getDomainData(),
primaryType: 'Operation',
message: operationData,
};
return this.ethSignTypedDataInternal(operation.signer, data, signingMethod);
}
async ethSignTypedCancelOperationInternal(operationHash, signer, signingMethod) {
const data = {
types: {
EIP712Domain: SignatureHelper_1.EIP712_DOMAIN_STRUCT,
CancelOperation: EIP712_CANCEL_OPERATION_STRUCT,
},
domain: this.getDomainData(),
primaryType: 'CancelOperation',
message: {
action: 'Cancel Operations',
operationHashes: [operationHash],
},
};
return this.ethSignTypedDataInternal(signer, data, signingMethod);
}
}
exports.SignedOperations = SignedOperations;
//# sourceMappingURL=SignedOperations.js.map