diff --git a/.gitignore b/.gitignore index e19c18a..924eb59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.swp *.swo +.DS_Store # Logs logs diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..970d890 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v12.16.3 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eea84fc..004a284 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@centrifuge/tinlake-js", - "version": "0.0.19-develop.5", + "version": "0.0.19-develop.17", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -216,15 +216,6 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, - "@types/ethjs-signer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@types/ethjs-signer/-/ethjs-signer-0.1.0.tgz", - "integrity": "sha512-Bom/6SlljFQexWsxYnMLcR18kQxUcqqfXWct7c1ZMNXYzyNNsF5RP60x7cdAhURnlYEmiTGCTO58YEhe063YbA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -356,11 +347,6 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, - "array-uniq": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.2.tgz", - "integrity": "sha1-X8w3OSB3VyPP1k1lxkvvU7+eum0=" - }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -391,15 +377,6 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -744,11 +721,6 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, - "core-js": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", - "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -954,17 +926,6 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, - "elliptic": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.3.2.tgz", - "integrity": "sha1-5MgeCCnPCmWrcOmYuCMnI7XBvEg=", - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "inherits": "^2.0.1" - } - }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -1119,197 +1080,6 @@ } } }, - "ethjs": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.4.0.tgz", - "integrity": "sha512-UnQeRMpQ+JETN2FviexEskUwByid+eO8rybjPnk2DNUzjUn0VKNrUbiCAud7Es6otDFwjUeOS58vMZwkZxIIog==", - "requires": { - "bn.js": "4.11.6", - "ethjs-abi": "0.2.1", - "ethjs-contract": "0.2.3", - "ethjs-filter": "0.1.8", - "ethjs-provider-http": "0.1.6", - "ethjs-query": "0.3.8", - "ethjs-unit": "0.1.6", - "ethjs-util": "0.1.3", - "js-sha3": "0.5.5", - "number-to-bn": "1.7.0" - } - }, - "ethjs-abi": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.1.tgz", - "integrity": "sha1-4KepOn6BFjqUR3utVu3lJKtt5TM=", - "requires": { - "bn.js": "4.11.6", - "js-sha3": "0.5.5", - "number-to-bn": "1.7.0" - } - }, - "ethjs-account": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/ethjs-account/-/ethjs-account-0.1.4.tgz", - "integrity": "sha1-J6g3hr0MM7FiW+hgjcIrAJ2/e4c=", - "requires": { - "elliptic": "6.3.2", - "ethjs-sha3": "0.1.2", - "randomhex": "0.1.5", - "strip-hex-prefix": "^1.0.0" - } - }, - "ethjs-contract": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.2.3.tgz", - "integrity": "sha512-fKsHm57wxwHrZhVlD8AHU2lC2G3c1fmvoEz15BpqIkuGWiTbjuvrQo2Avc+3EQpSsTFWNdyxC0h1WKRcn5kkyQ==", - "requires": { - "babel-runtime": "^6.26.0", - "ethjs-abi": "0.2.0", - "ethjs-filter": "0.1.8", - "ethjs-util": "0.1.3", - "js-sha3": "0.5.5" - }, - "dependencies": { - "ethjs-abi": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.0.tgz", - "integrity": "sha1-0+LCIQEVIPxJm3FoIDbBT8wvWyU=", - "requires": { - "bn.js": "4.11.6", - "js-sha3": "0.5.5", - "number-to-bn": "1.7.0" - } - } - } - }, - "ethjs-filter": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.8.tgz", - "integrity": "sha512-qTDPskDL2UadHwjvM8A+WG9HwM4/FoSY3p3rMJORkHltYcAuiQZd2otzOYKcL5w2Q3sbAkW/E3yt/FPFL/AVXA==" - }, - "ethjs-format": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/ethjs-format/-/ethjs-format-0.2.7.tgz", - "integrity": "sha512-uNYAi+r3/mvR3xYu2AfSXx5teP4ovy9z2FrRsblU+h2logsaIKZPi9V3bn3V7wuRcnG0HZ3QydgZuVaRo06C4Q==", - "requires": { - "bn.js": "4.11.6", - "ethjs-schema": "0.2.1", - "ethjs-util": "0.1.3", - "is-hex-prefixed": "1.0.0", - "number-to-bn": "1.7.0", - "strip-hex-prefix": "1.0.0" - } - }, - "ethjs-provider-http": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ethjs-provider-http/-/ethjs-provider-http-0.1.6.tgz", - "integrity": "sha1-HsXZtL4lfvHValALIqdBmF6IlCA=", - "requires": { - "xhr2": "0.1.3" - } - }, - "ethjs-provider-signer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/ethjs-provider-signer/-/ethjs-provider-signer-0.1.4.tgz", - "integrity": "sha1-a9XLOKjVsN30asHiOmDuoXFhca4=", - "requires": { - "ethjs-provider-http": "0.1.6", - "ethjs-rpc": "0.1.2" - }, - "dependencies": { - "ethjs-format": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/ethjs-format/-/ethjs-format-0.1.8.tgz", - "integrity": "sha1-kl7N2WXqcqKi2vKhIuW/gLWtUio=", - "requires": { - "bn.js": "4.11.6", - "ethjs-schema": "0.1.4", - "ethjs-util": "0.1.3", - "is-hex-prefixed": "1.0.0", - "number-to-bn": "1.7.0", - "strip-hex-prefix": "1.0.0" - } - }, - "ethjs-rpc": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.1.2.tgz", - "integrity": "sha1-OaNFa1HFmu6vtbpVZYmlny2ojSY=", - "requires": { - "ethjs-format": "0.1.8" - } - }, - "ethjs-schema": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/ethjs-schema/-/ethjs-schema-0.1.4.tgz", - "integrity": "sha1-AyOhYzOxrOmo8daWpu5jRI/dRV8=" - } - } - }, - "ethjs-query": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.8.tgz", - "integrity": "sha512-/J5JydqrOzU8O7VBOwZKUWXxHDGr46VqNjBCJgBVNNda+tv7Xc8Y2uJc6aMHHVbeN3YOQ7YRElgIc0q1CI02lQ==", - "requires": { - "babel-runtime": "^6.26.0", - "ethjs-format": "0.2.7", - "ethjs-rpc": "0.2.0", - "promise-to-callback": "^1.0.0" - } - }, - "ethjs-rpc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.2.0.tgz", - "integrity": "sha512-RINulkNZTKnj4R/cjYYtYMnFFaBcVALzbtEJEONrrka8IeoarNB9Jbzn+2rT00Cv8y/CxAI+GgY1d0/i2iQeOg==", - "requires": { - "promise-to-callback": "^1.0.0" - } - }, - "ethjs-schema": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ethjs-schema/-/ethjs-schema-0.2.1.tgz", - "integrity": "sha512-DXd8lwNrhT9sjsh/Vd2Z+4pfyGxhc0POVnLBUfwk5udtdoBzADyq+sK39dcb48+ZU+2VgtwHxtGWnLnCfmfW5g==" - }, - "ethjs-sha3": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ethjs-sha3/-/ethjs-sha3-0.1.2.tgz", - "integrity": "sha1-4D9sNKPEIGvewo1BdNbX/wZqFdI=", - "requires": { - "hash.js": "1.0.3" - }, - "dependencies": { - "hash.js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz", - "integrity": "sha1-EzL/ABVsCg/92CNgE9B7d6BFFXM=", - "requires": { - "inherits": "^2.0.1" - } - } - } - }, - "ethjs-signer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ethjs-signer/-/ethjs-signer-0.1.1.tgz", - "integrity": "sha1-Cvd5YeKe5FhgOqvTZguIaNM4ZEE=", - "requires": { - "elliptic": "6.3.2", - "js-sha3": "0.5.5", - "number-to-bn": "1.1.0", - "rlp": "2.0.0", - "strip-hex-prefix": "1.0.0" - }, - "dependencies": { - "number-to-bn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.1.0.tgz", - "integrity": "sha1-UaM4fFvGgDWrQFjGJhMvdn2dCL8=", - "requires": { - "bn.js": "4.11.6", - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" - } - } - } - }, "ethjs-unit": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", @@ -1319,15 +1089,6 @@ "number-to-bn": "1.7.0" } }, - "ethjs-util": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", - "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=", - "requires": { - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" - } - }, "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", @@ -2200,8 +1961,7 @@ "glpk.js": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/glpk.js/-/glpk.js-3.1.2.tgz", - "integrity": "sha512-gvPFcrjnBeOTVGNH98KjcEg7pbh/RG6mObWlXvmWuIlPVkow6YPSlif1UqJA6B2p/q69lOSyLtXt7qyQhZEmFw==", - "dev": true + "integrity": "sha512-gvPFcrjnBeOTVGNH98KjcEg7pbh/RG6mObWlXvmWuIlPVkow6YPSlif1UqJA6B2p/q69lOSyLtXt7qyQhZEmFw==" }, "got": { "version": "6.7.1", @@ -2668,11 +2428,6 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "is-fn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fn/-/is-fn-1.0.0.tgz", - "integrity": "sha1-lUPV3nvPWwiiLsiiC65uKG1RDYw=" - }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -2828,11 +2583,6 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, - "js-sha3": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz", - "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko=" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3668,15 +3418,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "promise-to-callback": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/promise-to-callback/-/promise-to-callback-1.0.0.tgz", - "integrity": "sha1-XSp0kBC/tn2WNZj805YHRqaP7vc=", - "requires": { - "is-fn": "^1.0.0", - "set-immediate-shim": "^1.0.1" - } - }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -3721,14 +3462,6 @@ "resolved": "https://registry.npmjs.org/randomhex/-/randomhex-0.1.5.tgz", "integrity": "sha1-us7vmCMpCRQA8qKRLGzQLxCU9YU=" }, - "randomstring": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/randomstring/-/randomstring-1.1.5.tgz", - "integrity": "sha1-bfBij3XL1ZMpMNn+OrTpVqGFGMM=", - "requires": { - "array-uniq": "1.0.2" - } - }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3782,11 +3515,6 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", "dev": true }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -3871,11 +3599,6 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, - "rlp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.0.0.tgz", - "integrity": "sha1-nbOE/0uJqPYVY9kjldhiWxjzr7A=" - }, "rollup": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.16.3.tgz", @@ -4047,11 +3770,6 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" - }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -5098,11 +4816,6 @@ "xhr-request": "^1.0.1" } }, - "xhr2": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.3.tgz", - "integrity": "sha1-y/xHWaabSoiOeM9PILBRA4dXvRE=" - }, "xmlhttprequest": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", diff --git a/package.json b/package.json index 178d124..fafbc0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@centrifuge/tinlake-js", - "version": "0.0.19-develop.5", + "version": "0.0.19-develop.17", "description": "Centrifuge Tinlake Contracts Client", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -13,24 +13,18 @@ "decimal.js-light": "^2.5.0", "dotenv": "^8.2.0", "ethers": "4.0.45", - "ethjs": "^0.4.0", - "ethjs-account": "^0.1.4", - "ethjs-provider-signer": "^0.1.4", - "ethjs-signer": "^0.1.1", + "glpk.js": "3.1.2", "mocha": "^6.1.4", - "randomstring": "^1.1.5", "web3-eth-abi": "^1.2.11", "web3-utils": "^1.2.0" }, "devDependencies": { "@types/assert": "^1.4.2", "@types/bn.js": "^4.11.6", - "@types/ethjs-signer": "^0.1.0", "@types/mocha": "^5.2.7", "@types/node": "^13.11.0", "@types/web3-eth-abi": "^1.0.0", "declaration-bundler-webpack-plugin": "^1.0.3", - "glpk.js": "3.1.2", "husky": "^4.2.3", "nodemon": "^1.19.1", "prettier": "2.1.1", @@ -49,7 +43,7 @@ "scripts": { "build": "rollup -c", "start": "rollup -cw", - "test": "ts-mocha -p src/test/tsconfig.json src/**/*.spec.ts src/*.spec.ts --timeout 40000", + "test": "ts-mocha -p src/test/tsconfig.json src/**/*.spec.ts --timeout 80000", "nodemon": "nodemon node inspect dist/Tinlake.js", "generate-docs": "typedoc --out docs --exclude \"./node_modules/**\" --exclude \"./src/abi/**\" --exclude \"./src/index.ts\" --exclude \"./src/actions/*.spec.ts\" --exclude \"./src/test/**\" --excludeExternals --excludeNotExported --ignoreCompilerErrors ./src", "lint": "tslint --project .", diff --git a/src/Tinlake.ts b/src/Tinlake.ts index 6b550fa..996e88b 100644 --- a/src/Tinlake.ts +++ b/src/Tinlake.ts @@ -1,14 +1,13 @@ -import Eth from 'ethjs' -import { ethI } from './services/ethereum' import abiDefinitions from './abi' import { ethers } from 'ethers' +import BN from 'bn.js' const contractNames = [ 'TINLAKE_CURRENCY', 'JUNIOR_OPERATOR', - 'JUNIOR', + 'JUNIOR_TRANCHE', 'JUNIOR_TOKEN', - 'SENIOR', + 'SENIOR_TRANCHE', 'SENIOR_TOKEN', 'SENIOR_OPERATOR', 'DISTRIBUTOR', @@ -21,7 +20,6 @@ const contractNames = [ 'THRESHOLD', 'PRICE_POOL', 'COLLATERAL_NFT', - 'COLLATERAL_NFT_DATA', 'ROOT_CONTRACT', 'PROXY', 'PROXY_REGISTRY', @@ -30,92 +28,70 @@ const contractNames = [ 'LENDER_DEPLOYER', 'NFT_FEED', 'GOVERNANCE', -] - -type AbiOutput = { - name: string - type: 'address' | 'uint256' -} - -export type EthConfig = { - from?: string - gasPrice?: string - gas?: string + 'ALLOWANCE_OPERATOR', +] as const + +export type PendingTransaction = { + hash?: string + status: number + error?: string + timesOutAt?: number + // receipt: () => Promise } -export type EthersConfig = { - provider?: ethers.providers.Provider - signer?: ethers.Signer -} - -export type ContractNames = typeof contractNames[number] +export type ContractName = typeof contractNames[number] export type Contracts = { - [key in ContractNames]?: any + [key in ContractName]?: ethers.Contract } export type ContractAbis = { - [key in ContractNames]?: any + [key in ContractName]?: (ethers.utils.EventFragment | ethers.utils.FunctionFragment)[] } export type ContractAddresses = { - [key in ContractNames]?: string + [key in ContractName]?: string } export type TinlakeParams = { - provider: any - transactionTimeout: number + provider: ethers.providers.Provider + signer?: ethers.Signer + transactionTimeout?: number contractAddresses?: ContractAddresses | {} contractAbis?: ContractAbis | {} - ethConfig?: EthConfig - ethersConfig?: EthersConfig - ethOptions?: any | {} + overrides?: ethers.providers.TransactionRequest contracts?: Contracts | {} contractConfig?: any | {} } export type Constructor = new (...args: any[]) => Tinlake +ethers.errors.setLogLevel('error') + +// This adds a .toBN() function to all BigNumber instances returned by ethers.js +;(ethers.utils.BigNumber as any).prototype.toBN = function () { + return new BN((this as any).toString()) +} + export default class Tinlake { - public provider: any - public eth: ethI - public ethOptions: any - public ethConfig: EthConfig - public ethersConfig: EthersConfig + public provider: ethers.providers.Provider + public signer?: ethers.Signer + public overrides: ethers.providers.TransactionRequest = {} public contractAddresses: ContractAddresses public transactionTimeout: number public contracts: Contracts = {} public contractAbis: ContractAbis = {} public contractConfig: any = {} + public readonly version: number = 2 constructor(params: TinlakeParams) { - const { - provider, - contractAddresses, - transactionTimeout, - contractAbis, - ethOptions, - ethConfig, - ethersConfig, - contractConfig, - } = params - if (!contractAbis) { - this.contractAbis = abiDefinitions - } - + const { provider, signer, contractAddresses, transactionTimeout, contractAbis, overrides, contractConfig } = params + this.contractAbis = contractAbis || abiDefinitions this.contractConfig = contractConfig || {} this.contractAddresses = contractAddresses || {} - this.transactionTimeout = transactionTimeout - this.setProvider(provider, ethOptions) - this.setEthConfig(ethConfig || {}) - this.setEthersConfig(ethersConfig || {}) - } - - setProvider = (provider: any, ethOptions?: any) => { - this.provider = provider - this.ethOptions = ethOptions || {} - this.eth = new Eth(this.provider, this.ethOptions) as ethI - + this.transactionTimeout = transactionTimeout || 3600 + this.overrides = overrides || {} + this.setProviderAndSigner(provider, signer) this.setContracts() } @@ -123,7 +99,7 @@ export default class Tinlake { // set root & proxy contracts contractNames.forEach((name) => { if (this.contractAbis[name] && this.contractAddresses[name]) { - this.contracts[name] = this.eth.contract(this.contractAbis[name]).at(this.contractAddresses[name]) + this.contracts[name] = this.createContract(this.contractAddresses[name]!, name) } }) @@ -140,23 +116,104 @@ export default class Tinlake { } } - setEthConfig = (ethConfig: EthConfig) => { - this.ethConfig = { - ...this.ethConfig, - ...ethConfig, + setProviderAndSigner = (provider: ethers.providers.Provider, signer?: ethers.Signer) => { + this.provider = provider + this.signer = signer + } + + createContract(address: string, abiName: ContractName) { + return new ethers.Contract(address, this.contractAbis[abiName]!, this.provider) + } + + contract(abiName: keyof Tinlake['contracts'] | ContractName, address?: string) { + const signerOrProvider = this.signer || this.provider + if (!(abiName in this.contracts) && !(address && abiName in this.contractAbis)) { + throw new Error(`Contract ${abiName} not loaded: ${JSON.stringify(Object.keys(this.contracts))}`) } + + if (address) { + // If an address was passed, return a contract at that specific address + return new ethers.Contract(address, this.contractAbis[abiName]!, signerOrProvider) + } + + if (this.signer) { + // Return the prespecified contract for that name, and connect it to the signer so that transactions can be initiated + return this.contracts[abiName]!.connect(signerOrProvider) + } + + // Return the prespecified contract for that name, without a signer, which means that you can only retrieve information, not initiate transactions + return this.contracts[abiName]! } - setEthersConfig = (ethersConfig: EthersConfig) => { - this.ethersConfig = { - ...this.ethersConfig, - ...ethersConfig, + /** + * Handle timeout and wait for transaction success/failure + * @param txPromise + */ + async pending(txPromise: Promise): Promise { + try { + const tx = await txPromise + const timesOutAt = Date.now() + this.transactionTimeout * 1000 + return { + timesOutAt, + status: 1, + hash: tx.hash, + // receipt: async () => { + // return new Promise(async (resolve, reject) => { + // if (!tx.hash) return reject(tx) + + // let timer: NodeJS.Timer | undefined = undefined + // if (timesOutAt) { + // timer = setTimeout(() => { + // return reject(`Transaction ${tx.hash} timed out at ${timesOutAt}`) + // }, timesOutAt - Date.now()) + // } + + // try { + // const receipt = await this.provider!.waitForTransaction(tx.hash) + // if (timer) clearTimeout(timer) + + // return resolve(receipt) + // } catch (e) { + // if (timer) clearTimeout(timer) + // console.error(`Error caught in tinlake.getTransactionReceipt(): ${JSON.stringify(e)}`) + // return reject() + // } + // }) + // }, + } + } catch (e) { + console.error(`Error caught in tinlake.pending(): ${JSON.stringify(e)}`) + return { + status: 0, + error: e.message, + // receipt: async () => { + // return Promise.reject('Error caught in tinlake.pending()') + // }, + } } } - createContract(address: string, abiName: string) { - const contract = this.eth.contract(this.contractAbis[abiName]).at(address) - return contract + async getTransactionReceipt(tx: PendingTransaction): Promise { + return new Promise(async (resolve, reject) => { + if (!tx.hash) return reject(tx) + + let timer: NodeJS.Timer | undefined = undefined + if (tx.timesOutAt) { + timer = setTimeout(() => { + return reject(`Transaction ${tx.hash} timed out at ${tx.timesOutAt}`) + }, tx.timesOutAt - Date.now()) + } + + try { + const receipt = await this.provider!.waitForTransaction(tx.hash) + if (timer) clearTimeout(timer) + + return resolve(receipt) + } catch (e) { + console.error(`Error caught in tinlake.getTransactionReceipt(): ${JSON.stringify(e)}`) + return reject() + } + }) } getOperatorType = (tranche: string) => { diff --git a/src/abi/.DS_Store b/src/abi/.DS_Store deleted file mode 100644 index 98f5e5a..0000000 Binary files a/src/abi/.DS_Store and /dev/null differ diff --git a/src/abi/NftData.abi.json b/src/abi/NftData.abi.json deleted file mode 100644 index 3f45b07..0000000 --- a/src/abi/NftData.abi.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "constant":true, - "inputs":[ - { - "name":"", - "type":"uint256" - } - ], - "name":"data", - "outputs":{ - - }, - "payable":false, - "stateMutability":"view", - "type":"function" - } -] diff --git a/src/abi/index.ts b/src/abi/index.ts index b5f4338..980a243 100644 --- a/src/abi/index.ts +++ b/src/abi/index.ts @@ -15,16 +15,13 @@ import contractAbiProxy from './Proxy.abi.json' import contractAbiProxyRegistry from './ProxyRegistry.abi.json' import contractAbiTranche from './Tranche.abi.json' import contractAbiSeniorTranche from './SeniorTranche.abi.json' -import contractAbiNFTData from './NftData.abi.json' import contractAbiNFT from './test/SimpleNFT.abi.json' import contractAbiBorrowerDeployer from './BorrowerDeployer.abi.json' import contractAbiLenderDeployer from './LenderDeployer.abi.json' import { ContractAbis } from '../Tinlake' export default { - // COLLATERAL_NFT : contractAbiTitle, COLLATERAL_NFT: contractAbiNFT, - COLLATERAL_NFT_DATA: contractAbiNFTData, TITLE: contractAbiTitle, TINLAKE_CURRENCY: contractAbiCurrency, SHELF: contractAbiShelf, @@ -43,8 +40,8 @@ export default { ACTIONS: contractAbiActions, ALLOWANCE_OPERATOR: contractAbiAllowanceOperator, PROPORTIONAL_OPERATOR: contractAbiProportionalOperator, - JUNIOR: contractAbiTranche, - SENIOR: contractAbiSeniorTranche, + JUNIOR_TRANCHE: contractAbiTranche, + SENIOR_TRANCHE: contractAbiSeniorTranche, BORROWER_DEPLOYER: contractAbiBorrowerDeployer, LENDER_DEPLOYER: contractAbiLenderDeployer, NFT_FEED: contractAbiNftFeed, diff --git a/src/actions/admin.spec.ts b/src/actions/admin.spec.ts index 0cb67d1..e4920db 100644 --- a/src/actions/admin.spec.ts +++ b/src/actions/admin.spec.ts @@ -1,15 +1,13 @@ -const randomString = require('randomstring') -const account = require('ethjs-account') import { ITinlake } from '../types/tinlake' import assert from 'assert' import { createTinlake, TestProvider } from '../test/utils' import testConfig from '../test/config' -import { interestRateToFee } from '../utils/interestRateToFee' +import { ethers } from 'ethers' const testProvider = new TestProvider(testConfig) -const adminAccount = account.generate(randomString.generate(32)) -const borrowerAccount = account.generate(randomString.generate(32)) -const lenderAccount = account.generate(randomString.generate(32)) +const adminAccount = ethers.Wallet.createRandom() +const borrowerAccount = ethers.Wallet.createRandom() +const lenderAccount = ethers.Wallet.createRandom() const { SUCCESS_STATUS, FAUCET_AMOUNT, contractAddresses } = testConfig let adminTinlake: ITinlake let governanceTinlake: ITinlake @@ -28,19 +26,19 @@ describe('admin tests', async () => { describe('operator', async () => { it('success: set allowance for junior investor', async () => { // rely admin on junior operator - await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['JUNIOR_OPERATOR']) - const maxCurrency = '1000' - const maxToken = '100' + const relyTx = await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['JUNIOR_OPERATOR']) + await governanceTinlake.getTransactionReceipt(relyTx) + + const maxCurrency = '10' + const maxToken = '1' // set allowance for lender address - const allowanceResult: any = await adminTinlake.approveAllowanceJunior( - lenderAccount.address, - maxCurrency, - maxToken - ) + const allowanceTx = await adminTinlake.approveAllowanceJunior(lenderAccount.address, maxCurrency, maxToken) + const allowanceResult = await allowanceTx.receipt() const maxSupplyAmount = await adminTinlake.getMaxSupplyAmountJunior(lenderAccount.address) const maxRedeemAmount = await adminTinlake.getMaxRedeemAmountJunior(lenderAccount.address) + assert.equal(allowanceResult.status, SUCCESS_STATUS) assert.equal(maxRedeemAmount, maxToken) assert.equal(maxSupplyAmount, maxCurrency) diff --git a/src/actions/admin.ts b/src/actions/admin.ts index 3fb14c9..44e9495 100644 --- a/src/actions/admin.ts +++ b/src/actions/admin.ts @@ -1,5 +1,5 @@ -import { ContractNames, Constructor, TinlakeParams } from '../Tinlake' -import { waitAndReturnEvents, executeAndRetry, ZERO_ADDRESS } from '../services/ethereum' +import { ContractName, Constructor, TinlakeParams, PendingTransaction } from '../Tinlake' +import { ZERO_ADDRESS } from '../services/ethereum' import BN from 'bn.js' const web3 = require('web3-utils') @@ -7,183 +7,118 @@ export function AdminActions>(Bas return class extends Base implements IAdminActions { canQueryPermissions = () => { return ( - !!this.contracts['PILE']?.wards && - !!this.contracts['SENIOR']?.wards && - !!this.contracts['PRICE_POOL']?.wards && - !!this.contracts['ASSESSOR']?.wards && - !!this.contracts['JUNIOR_OPERATOR']?.wards && - !!this.contracts['SENIOR_OPERATOR']?.wards && - !!this.contracts['COLLECTOR']?.wards + !!this.contract('PILE')?.wards && + !!this.contract('SENIOR_TRANCHE')?.wards && + !!this.contract('PRICE_POOL')?.wards && + !!this.contract('ASSESSOR')?.wards && + !!this.contract('JUNIOR_OPERATOR')?.wards && + !!this.contract('SENIOR_OPERATOR')?.wards && + !!this.contract('COLLECTOR')?.wards ) } - isWard = async (user: string, contractName: ContractNames) => { - if (!this.contracts[contractName]?.wards) { - return new BN(0) - } - const res: { 0: BN } = await executeAndRetry(this.contracts[contractName].wards, [user]) - return res[0] + isWard = async (user: string, contractName: ContractName) => { + if (!this.contract(contractName)?.wards) return new BN(0) + return (await this.contract(contractName).wards(user)).toBN() } canSetInterestRate = async (user: string) => { - if (!this.contracts['PILE']?.wards) { - return false - } - const res: { 0: BN } = await executeAndRetry(this.contracts['PILE'].wards, [user]) - return res[0].toNumber() === 1 + if (!this.contract('PILE')?.wards) return false + return (await this.contract('PILE').wards(user)).toBN().toNumber() === 1 } canSetSeniorTrancheInterest = async (user: string) => { - if (this.contractAddresses['SENIOR'] !== ZERO_ADDRESS) { - if (!this.contracts['SENIOR']?.wards) { - return false - } - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR'].wards, [user]) - return res[0].toNumber() === 1 - } - return false + if (!(this.contractAddresses['SENIOR_TRANCHE'] !== ZERO_ADDRESS)) return false + if (!this.contract('SENIOR_TRANCHE')?.wards) return false + return (await this.contract('SENIOR_TRANCHE').wards(user)).toBN().toNumber() === 1 } canSetRiskScore = async (user: string) => { - if (!this.contracts['PRICE_POOL']?.wards) { - return false - } - const res: { 0: BN } = await executeAndRetry(this.contracts['PRICE_POOL'].wards, [user]) - return res[0].toNumber() === 1 + if (!this.contract('PRICE_POOL')?.wards) return false + return (await this.contract('PRICE_POOL').wards(user)).toBN().toNumber() === 1 } // lender permissions (note: allowance operator for default deployment) canSetMinimumJuniorRatio = async (user: string) => { - if (!this.contracts['ASSESSOR']?.wards) { - return false - } - const res: { 0: BN } = await executeAndRetry(this.contracts['ASSESSOR'].wards, [user]) - return res[0].toNumber() === 1 + if (!this.contract('ASSESSOR')?.wards) return false + return (await this.contract('ASSESSOR').wards(user)).toBN().toNumber() === 1 } canSetInvestorAllowanceJunior = async (user: string) => { - if (!this.contracts['JUNIOR_OPERATOR']?.wards) { - return false - } - const res: { 0: BN } = await executeAndRetry(this.contracts['JUNIOR_OPERATOR'].wards, [user]) - return res[0].toNumber() === 1 + if (!this.contract('JUNIOR_OPERATOR')?.wards) return false + return (await this.contract('JUNIOR_OPERATOR').wards(user)).toBN().toNumber() === 1 } canSetInvestorAllowanceSenior = async (user: string) => { - if (!this.contracts['SENIOR_OPERATOR']?.wards) { - return false - } - if (this.contractAddresses['SENIOR_OPERATOR'] !== ZERO_ADDRESS) { - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].wards, [user]) - return res[0].toNumber() === 1 - } - return false + if (!this.contract('SENIOR_OPERATOR')?.wards) return false + if (!(this.contractAddresses['SENIOR_OPERATOR'] !== ZERO_ADDRESS)) return false + return (await this.contract('SENIOR_OPERATOR').wards(user)).toBN().toNumber() === 1 } canSetLoanPrice = async (user: string) => { - if (!this.contracts['COLLECTOR']?.wards) { - return false - } - const res: { 0: BN } = await executeAndRetry(this.contracts['COLLECTOR'].wards, [user]) - return res[0].toNumber() === 1 + if (!this.contract('COLLECTOR')?.wards) return false + return (await this.contract('COLLECTOR').wards(user)).toBN().toNumber() === 1 } // ------------ admin functions borrower-site ------------- existsRateGroup = async (ratePerSecond: string) => { const rateGroup = getRateGroup(ratePerSecond) - const res: { ratePerSecond: BN } = await executeAndRetry(this.contracts['PILE'].rates, [rateGroup]) - return !res.ratePerSecond.isZero() + const actualRate = (await this.contract('PILE').rates(rateGroup)).toBN() + return !actualRate.isZero() } initRate = async (ratePerSecond: string) => { const rateGroup = getRateGroup(ratePerSecond) - const txHash = await executeAndRetry(this.contracts['PILE'].file, [ - web3.fromAscii('rate'), - rateGroup, - ratePerSecond, - this.ethConfig, - ]) - console.log(`[Initialising rate] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['PILE'].abi, this.transactionTimeout) + // Source: https://github.com/ethereum/web3.js/issues/2256#issuecomment-462730550 + return this.pending( + this.contract('PILE').file(web3.fromAscii('rate').padEnd(66, '0'), rateGroup, ratePerSecond, this.overrides) + ) } changeRate = async (loan: string, ratePerSecond: string) => { const rateGroup = getRateGroup(ratePerSecond) - const txHash = await executeAndRetry(this.contracts['PILE'].changeRate, [loan, rateGroup, this.ethConfig]) - console.log(`[Initialising rate] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['PILE'].abi, this.transactionTimeout) + return this.pending(this.contract('PILE').changeRate(loan, rateGroup, this.overrides)) } setRate = async (loan: string, ratePerSecond: string) => { const rateGroup = getRateGroup(ratePerSecond) - const txHash = await executeAndRetry(this.contracts['PILE'].setRate, [loan, rateGroup, this.ethConfig]) - console.log(`[Setting rate] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['PILE'].abi, this.transactionTimeout) + return this.pending(this.contract('PILE').setRate(loan, rateGroup, this.overrides)) } // ------------ admin functions lender-site ------------- setMinimumJuniorRatio = async (ratio: string) => { - const txHash = await executeAndRetry(this.contracts['ASSESSOR'].file, [ - web3.fromAscii('minJuniorRatio'), - ratio, - this.ethConfig, - ]) - console.log(`[Assessor file] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['ASSESSOR'].abi, this.transactionTimeout) + // Source: https://github.com/ethereum/web3.js/issues/2256#issuecomment-462730550 + return this.pending( + this.contract('ASSESSOR').file(web3.fromAscii('minJuniorRatio').padEnd(66, '0'), ratio, this.overrides) + ) } approveAllowanceJunior = async (user: string, maxCurrency: string, maxToken: string) => { - const txHash = await executeAndRetry(this.contracts['JUNIOR_OPERATOR'].approve, [ - user, - maxCurrency, - maxToken, - this.ethConfig, - ]) - console.log(`[Approve allowance Junior] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['JUNIOR_OPERATOR'].abi, this.transactionTimeout) + return this.pending(this.contract('JUNIOR_OPERATOR').approve(user, maxCurrency, maxToken, this.overrides)) } approveAllowanceSenior = async (user: string, maxCurrency: string, maxToken: string) => { - const operatorType = this.getOperatorType('senior') - let txHash - switch (operatorType) { - case 'PROPORTIONAL_OPERATOR': - txHash = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].approve, [user, maxCurrency, this.ethConfig]) - break - // ALLOWANCE_OPERATOR - default: - txHash = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].approve, [ - user, - maxCurrency, - maxToken, - this.ethConfig, - ]) + if (this.getOperatorType('senior') === 'PROPORTIONAL_OPERATOR') { + return this.pending(this.contract('SENIOR_OPERATOR').approve(user, maxCurrency, this.overrides)) } - console.log(`[Approve allowance Senior] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SENIOR_OPERATOR'].abi, this.transactionTimeout) + return this.pending(this.contract('SENIOR_OPERATOR').approve(user, maxCurrency, maxToken, this.overrides)) } updateNftFeed = async (tokenId: string, value: number, riskGroup?: number) => { - let txHash; if (!riskGroup) { - txHash = await executeAndRetry(this.contracts['NFT_FEED'].update, [tokenId, value, this.ethConfig]); - } else { - txHash = await executeAndRetry(this.contracts['NFT_FEED'].update, [tokenId, value, riskGroup, this.ethConfig]); + return this.pending(this.contract('NFT_FEED')['update(bytes32,uint256)'](tokenId, value, this.overrides)) } - console.log(`[Updating NFT Feed] txHash: ${txHash}`); - return waitAndReturnEvents(this.eth, txHash, this.contracts['NFT_FEED'].abi, this.transactionTimeout) + return this.pending( + this.contract('NFT_FEED')['update(bytes32,uint256,uint256)'](tokenId, value, riskGroup, this.overrides) + ) } getNftFeedId = async (registry: string, tokenId: number) => { - const res = await executeAndRetry(this.contracts['NFT_FEED'].nftID, [registry, tokenId, this.ethConfig]); - console.log(`[Getting NFT ID]`); - return res[0] + return await this.contract('NFT_FEED')['nftID(address,uint256)'](registry, tokenId) } getNftFeedValue = async (nftFeedId: string) => { - const res = await executeAndRetry(this.contracts['NFT_FEED'].nftValues, [nftFeedId, this.ethConfig]); - console.log(`[Getting NFT Value]`); - return res[0] + return (await this.contract('NFT_FEED').nftValues(nftFeedId)).toBN() } } } @@ -194,7 +129,7 @@ function getRateGroup(ratePerSecond: string) { } export type IAdminActions = { - isWard(user: string, contractName: ContractNames): Promise + isWard(user: string, contractName: ContractName): Promise canSetInterestRate(user: string): Promise canSetSeniorTrancheInterest(user: string): Promise canSetMinimumJuniorRatio(user: string): Promise @@ -202,12 +137,14 @@ export type IAdminActions = { canSetInvestorAllowanceJunior(user: string): Promise canSetInvestorAllowanceSenior(user: string): Promise canSetLoanPrice(user: string): Promise - initRate(rate: string): Promise - setRate(loan: string, rate: string): Promise - setMinimumJuniorRatio(amount: string): Promise - approveAllowanceJunior(user: string, maxCurrency: string, maxToken: string): Promise - approveAllowanceSenior(user: string, maxCurrency: string, maxToken: string): Promise - updateNftFeed(nftId: string, value: number, riskGroup?: number): Promise + existsRateGroup(ratePerSecond: string): Promise + initRate(rate: string): Promise + setRate(loan: string, rate: string): Promise + changeRate(loan: string, ratePerSecond: string): Promise + setMinimumJuniorRatio(amount: string): Promise + approveAllowanceJunior(user: string, maxCurrency: string, maxToken: string): Promise + approveAllowanceSenior(user: string, maxCurrency: string, maxToken: string): Promise + updateNftFeed(nftId: string, value: number, riskGroup?: number): Promise getNftFeedId(registry: string, tokenId: number): Promise getNftFeedValue(tokenId: string): Promise } diff --git a/src/actions/analytics.ts b/src/actions/analytics.ts index 48e0f79..4813be8 100644 --- a/src/actions/analytics.ts +++ b/src/actions/analytics.ts @@ -1,5 +1,5 @@ import { Constructor, TinlakeParams } from '../Tinlake' -import { executeAndRetry, ZERO_ADDRESS } from '../services/ethereum' +import { ZERO_ADDRESS } from '../services/ethereum' import { Loan, Investor } from '../types/tinlake' import BN from 'bn.js' @@ -7,56 +7,49 @@ export function AnalyticsActions> return class extends Base implements IAnalyticsActions { // borrower analytics getTotalDebt = async (): Promise => { - const res: { 0: BN } = await executeAndRetry(this.contracts['PILE'].total, []) - return res[0] + return (await this.contract('PILE').total()).toBN() } getTotalBalance = async (): Promise => { - const res: { 0: BN } = await executeAndRetry(this.contracts['SHELF'].balance, []) - return res[0] + return (await this.contract('SHELF').balance()).toBN() } getPrincipal = async (loanId: string): Promise => { - const res = await executeAndRetry(this.contracts['CEILING'].ceiling, [loanId]) - return res ? res[0] : new BN(0) + return (await this.contract('CEILING').ceiling(loanId)).toBN() } - getDebt = async (loanID: string): Promise => { - const res = await executeAndRetry(this.contracts['PILE'].debt, [loanID]) - return res ? res[0] : new BN(0) + getDebt = async (loanId: string): Promise => { + return (await this.contract('PILE').debt(loanId)).toBN() } loanCount = async (): Promise => { - const res: { 0: BN } = await executeAndRetry(this.contracts['TITLE'].count, []) - return res[0] + return (await this.contract('TITLE').count()).toBN() } getCollateral = async (loanId: string): Promise => { - const res = await executeAndRetry(this.contracts['SHELF'].shelf, [loanId]) - return res + return await this.contract('SHELF').shelf(loanId) } - getOwnerOfCollateral = async (nftRegistryAddr: string, tokenId: string): Promise => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftRegistryAddr) - const res: { 0: BN } = await executeAndRetry(nft.ownerOf, [tokenId]) - return res[0] + getOwnerOfCollateral = async (nftRegistryAddress: string, tokenId: string): Promise => { + return this.contract('COLLATERAL_NFT', nftRegistryAddress).ownerOf(tokenId) } getInterestRate = async (loanId: string): Promise => { // retrieve nftId = hash from tokenID & registry - const nftId = (await executeAndRetry(this.contracts['NFT_FEED'].nftID, [loanId]))[0] - // retrieve riskgroup fro nft - const riskGroupRes: { 0: BN } = await executeAndRetry(this.contracts['NFT_FEED'].risk, [nftId]) - const riskGroup = riskGroupRes[0] || new BN(0) - const res = await executeAndRetry(this.contracts['PILE'].rates, [riskGroup]) - return res ? res[2] : new BN(0) + const nftId = await this.contract('NFT_FEED').nftID(loanId) + + // retrieve riskgroup from nft + const riskGroup = await this.contract('NFT_FEED').risk(nftId) + + // retrieve rates for this risk group + const res = await this.contract('PILE').rates(riskGroup) + return res[2].toBN() } getOwnerOfLoan = async (loanId: string): Promise => { let address try { - const res = await executeAndRetry(this.contracts['TITLE'].ownerOf, [loanId]) - address = res[0] + address = await this.contract('TITLE').ownerOf(loanId) } catch (e) { address = ZERO_ADDRESS } @@ -64,7 +57,7 @@ export function AnalyticsActions> } getStatus = async (nftRegistryAddr: string, tokenId: string, loanId: string): Promise => { - if ((await this.getOwnerOfCollateral(nftRegistryAddr, tokenId)) === this.contracts['SHELF'].address) { + if ((await this.getOwnerOfCollateral(nftRegistryAddr, tokenId)).toString() === this.contracts['SHELF']!.address) { return 'ongoing' } if ((await this.getOwnerOfLoan(loanId)) === ZERO_ADDRESS) { @@ -107,7 +100,9 @@ export function AnalyticsActions> } // lender analytics - getInvestor = async (user: string): Promise => { + getInvestor = async (user: string): Promise => { + if (typeof user === 'undefined' || user === '') return undefined + const includeSenior = this.existsSenior() const tokenBalanceJunior = await this.getJuniorTokenBalance(user) const tokenBalanceSenior = (includeSenior && (await this.getSeniorTokenBalance(user))) || new BN(0) @@ -133,28 +128,23 @@ export function AnalyticsActions> } getJuniorTokenBalance = async (user: string) => { - const res: { 0: BN } = await executeAndRetry(this.contracts['JUNIOR_TOKEN'].balanceOf, [user]) - return res[0] + return (await this.contract('JUNIOR_TOKEN').balanceOf(user)).toBN() } - getJuniorTotalSupply = async (user: string) => { - const res: { 0: BN } = await executeAndRetry(this.contracts['JUNIOR_TOKEN'].totalSupply, []) - return res[0] + getJuniorTotalSupply = async () => { + return (await this.contract('JUNIOR_TOKEN').totalSupply()).toBN() } getMaxSupplyAmountJunior = async (user: string) => { - const res: { 0: BN } = await executeAndRetry(this.contracts['JUNIOR_OPERATOR'].maxCurrency, [user]) - return res[0] + return (await this.contract('JUNIOR_OPERATOR').maxCurrency(user)).toBN() } getMaxRedeemAmountJunior = async (user: string) => { - const res = await executeAndRetry(this.contracts['JUNIOR_OPERATOR'].maxToken, [user]) - return res[0] + return (await this.contract('JUNIOR_OPERATOR').maxToken(user)).toBN() } getTokenPriceJunior = async () => { - const res = await executeAndRetry(this.contracts['ASSESSOR'].calcTokenPrice, [this.contractAddresses['JUNIOR']]) - return res[0] + return (await this.contract('ASSESSOR').calcTokenPrice(this.contractAddresses['JUNIOR_TRANCHE'])).toBN() } existsSenior = () => { @@ -162,19 +152,13 @@ export function AnalyticsActions> } getSeniorTokenBalance = async (user: string) => { - if (!this.existsSenior()) { - return new BN(0) - } - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR_TOKEN'].balanceOf, [user]) - return res[0] + if (!this.existsSenior()) return new BN(0) + return (await this.contract('SENIOR_TOKEN').balanceOf(user)).toBN() } - getSeniorTotalSupply = async (user: string) => { - if (!this.existsSenior()) { - return new BN(0) - } - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR_TOKEN'].totalSupply, []) - return res[0] + getSeniorTotalSupply = async () => { + if (!this.existsSenior()) return new BN(0) + return (await this.contract('SENIOR_TOKEN').totalSupply()).toBN() } getMaxSupplyAmountSenior = async (user: string) => { @@ -184,16 +168,13 @@ export function AnalyticsActions> let maxSupply: BN switch (operatorType) { case 'PROPORTIONAL_OPERATOR': - const supplyLimitRes: { 0: BN } = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].supplyMaximum, [ - user, - ]) - const suppliedRes: { 0: BN } = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].tokenReceived, [user]) - maxSupply = supplyLimitRes[0].sub(suppliedRes[0]) + const supplyLimit = (await this.contract('SENIOR_OPERATOR').supplyMaximum(user)).toBN() + const supplied = (await this.contract('SENIOR_OPERATOR').tokenReceived(user)).toBN() + maxSupply = supplyLimit.sub(supplied) break case 'ALLOWANCE_OPERATOR': - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].maxCurrency, [user]) - maxSupply = res[0] + maxSupply = (await this.contract('SENIOR_OPERATOR').maxCurrency(user)).toBN() break default: maxSupply = new BN(0) @@ -208,15 +189,10 @@ export function AnalyticsActions> let maxRedeem: BN switch (operatorType) { case 'PROPORTIONAL_OPERATOR': - const redeemLimitRes: { 0: BN } = await executeAndRetry( - this.contracts['SENIOR_OPERATOR'].calcMaxRedeemToken, - [user] - ) - maxRedeem = redeemLimitRes[0] + maxRedeem = (await this.contract('SENIOR_OPERATOR').calcMaxRedeemToken(user)).toBN() break case 'ALLOWANCE_OPERATOR': - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].maxToken, [user]) - maxRedeem = res[0] + maxRedeem = (await this.contract('SENIOR_OPERATOR').maxToken(user)).toBN() break default: maxRedeem = new BN(0) @@ -232,17 +208,10 @@ export function AnalyticsActions> let tokenPrice: BN switch (operatorType) { case 'PROPORTIONAL_OPERATOR': - const customTokenPriceRes: { 0: BN } = await executeAndRetry( - this.contracts['SENIOR_OPERATOR'].calcTokenPrice, - [user] - ) - tokenPrice = customTokenPriceRes[0] + tokenPrice = (await this.contract('SENIOR_OPERATOR').calcTokenPrice(user)).toBN() break case 'ALLOWANCE_OPERATOR': - const res: { 0: BN } = await executeAndRetry(this.contracts['ASSESSOR'].calcTokenPrice, [ - this.contractAddresses['SENIOR'], - ]) - tokenPrice = res[0] + tokenPrice = (await this.contract('ASSESSOR').calcTokenPrice(this.contractAddresses['SENIOR_TRANCHE'])).toBN() break default: tokenPrice = new BN(0) @@ -251,47 +220,38 @@ export function AnalyticsActions> } getSeniorReserve = async () => { - if (this.contractAddresses['SENIOR'] !== ZERO_ADDRESS) { - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR'].balance, []) - return res[0] || new BN(0) + if (this.contractAddresses['SENIOR_TRANCHE'] !== ZERO_ADDRESS) { + return (await this.contract('SENIOR_TRANCHE').balance()).toBN() } return new BN(0) } getJuniorReserve = async () => { - const res: { 0: BN } = await executeAndRetry(this.contracts['JUNIOR'].balance, []) - return res[0] || new BN(0) + return (await this.contract('JUNIOR_TRANCHE').balance()).toBN() } getMinJuniorRatio = async () => { - const res: { 0: BN } = await executeAndRetry(this.contracts['ASSESSOR'].minJuniorRatio, []) - return res[0] || new BN(0) + return (await this.contract('ASSESSOR').minJuniorRatio()).toBN() } getCurrentJuniorRatio = async () => { - const res: { 0: BN } = await executeAndRetry(this.contracts['ASSESSOR'].currentJuniorRatio, []) - return res[0] || new BN(0) + return (await this.contract('ASSESSOR').currentJuniorRatio()).toBN() } getAssetValueJunior = async () => { - const res: { 0: BN } = await executeAndRetry(this.contracts['ASSESSOR'].calcAssetValue, [ - this.contractAddresses['JUNIOR'], - ]) - return res[0] || new BN(0) + return (await this.contract('ASSESSOR').calcAssetValue(this.contractAddresses['JUNIOR_TRANCHE'])).toBN() } getSeniorDebt = async () => { - if (this.contractAddresses['SENIOR'] !== ZERO_ADDRESS) { - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR'].debt, []) - return res[0] || new BN(0) + if (this.contractAddresses['SENIOR_TRANCHE'] !== ZERO_ADDRESS) { + return (await this.contract('SENIOR_TRANCHE').debt()).toBN() } return new BN(0) } getSeniorInterestRate = async () => { - if (this.contractAddresses['SENIOR'] !== ZERO_ADDRESS) { - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR'].ratePerSecond, []) - return res[0] || new BN(0) + if (this.contractAddresses['SENIOR_TRANCHE'] !== ZERO_ADDRESS) { + return (await this.contract('SENIOR_TRANCHE').ratePerSecond()).toBN() } return new BN(0) } @@ -309,12 +269,14 @@ export type IAnalyticsActions = { getPrincipal(loanId: string): Promise getInterestRate(loanId: string): Promise getOwnerOfLoan(loanId: string): Promise - getOwnerOfCollateral(nftRegistryAddr: string, tokenId: string, loanId: string): Promise + getOwnerOfCollateral(nftRegistryAddr: string, tokenId: string): Promise existsSenior(): boolean getJuniorReserve(): Promise getSeniorReserve(): Promise getJuniorTokenBalance(user: string): Promise getSeniorTokenBalance(user: string): Promise + getJuniorTotalSupply(): Promise + getSeniorTotalSupply(): Promise getMaxSupplyAmountJunior(user: string): Promise getMaxRedeemAmountJunior(user: string): Promise getMaxSupplyAmountSenior(user: string): Promise @@ -326,7 +288,7 @@ export type IAnalyticsActions = { getMinJuniorRatio(): Promise getCurrentJuniorRatio(): Promise getAssetValueJunior(): Promise - getInvestor(user: string): Promise + getInvestor(user: string): Promise } export default AnalyticsActions diff --git a/src/actions/borrower.spec.ts b/src/actions/borrower.spec.ts index 2a7483a..65f0366 100644 --- a/src/actions/borrower.spec.ts +++ b/src/actions/borrower.spec.ts @@ -1,14 +1,12 @@ import assert from 'assert' -const account = require('ethjs-account') -const randomString = require('randomstring') import testConfig from '../test/config' import { ITinlake } from '../types/tinlake' import { createTinlake, TestProvider } from '../test/utils' -import { Account } from '../test/types' import BN from 'bn.js' +import { ethers } from 'ethers' -const adminAccount = account.generate(randomString.generate(32)) -let borrowerAccount: Account +const adminAccount = ethers.Wallet.createRandom() +let borrowerAccount: ethers.Wallet // user with super powers can fund and rely accounts let governanceTinlake: ITinlake @@ -23,15 +21,17 @@ describe('borrower tests', async () => { before(async () => { governanceTinlake = createTinlake(testConfig.godAccount, testConfig) adminTinlake = createTinlake(adminAccount, testConfig) + // fund borrowerAccount with ETH await testProvider.fundAccountWithETH(adminAccount.address, FAUCET_AMOUNT) - const amount = '5000' + // supply tranche with money + const amount = '50' await fundTranche(amount) }) beforeEach(async () => { - borrowerAccount = account.generate(randomString.generate(32)) + borrowerAccount = ethers.Wallet.createRandom() borrowerTinlake = createTinlake(borrowerAccount, testConfig) await testProvider.fundAccountWithETH(borrowerAccount.address, FAUCET_AMOUNT) }) @@ -42,19 +42,25 @@ describe('borrower tests', async () => { it('success: close loan', async () => { const { loanId } = await mintIssue(borrowerAccount.address, borrowerTinlake) - const closeResult = await borrowerTinlake.close(loanId) + + const closeTx = await borrowerTinlake.close(loanId) + const closeResult = await borrowerTinlake.getTransactionReceipt(closeTx) + assert.equal(closeResult.status, SUCCESS_STATUS) }) it('success: lock nft', async () => { // mint nft & issue loan const { tokenId, loanId } = await mintIssue(borrowerAccount.address, borrowerTinlake) - await borrowerTinlake.approveNFT(testConfig.nftRegistry, tokenId, contractAddresses['SHELF']) + const approveTx = await borrowerTinlake.approveNFT(testConfig.nftRegistry, tokenId, contractAddresses['SHELF']) + await borrowerTinlake.getTransactionReceipt(approveTx) // lock nft - await borrowerTinlake.lock(loanId) + const lockTx = await borrowerTinlake.lock(loanId) + await borrowerTinlake.getTransactionReceipt(lockTx) + assert.equal( - await borrowerTinlake.getNFTOwner(testConfig.nftRegistry, tokenId), + (await borrowerTinlake.getNFTOwner(testConfig.nftRegistry, tokenId)).toLowerCase(), contractAddresses['SHELF'].toLowerCase() ) }) @@ -62,13 +68,16 @@ describe('borrower tests', async () => { it('success: unlock nft', async () => { // mint nft & issue loan const { tokenId, loanId } = await mintIssue(borrowerAccount.address, borrowerTinlake) - await borrowerTinlake.approveNFT(testConfig.nftRegistry, tokenId, contractAddresses['SHELF']) + const approveTx = await borrowerTinlake.approveNFT(testConfig.nftRegistry, tokenId, contractAddresses['SHELF']) + await borrowerTinlake.getTransactionReceipt(approveTx) // lock nft - await borrowerTinlake.lock(loanId) + const lockTx = await borrowerTinlake.lock(loanId) + await borrowerTinlake.getTransactionReceipt(lockTx) // unlock nft - await borrowerTinlake.unlock(loanId) + const unlockTx = await borrowerTinlake.unlock(loanId) + await borrowerTinlake.getTransactionReceipt(unlockTx) }) it('success: borrow', async () => { @@ -79,15 +88,24 @@ describe('borrower tests', async () => { it('success: repay', async () => { const amount = '1000' const { loanId } = await mintIssueBorrow(borrowerAccount.address, borrowerTinlake, amount) + // wait to secs so that interest can accrue await new Promise((r) => setTimeout(r, 3000)) + // mint extra currency so that borrower can repay loan with interest - await governanceTinlake.mintCurrency(borrowerAccount.address, FAUCET_AMOUNT) + const mintTx = await governanceTinlake.mintCurrency(borrowerAccount.address, FAUCET_AMOUNT) + await governanceTinlake.getTransactionReceipt(mintTx) + // repay loan const initialDebt = await borrowerTinlake.getDebt(loanId) + // approve shelf to take currency - await borrowerTinlake.approveCurrency(contractAddresses['SHELF'], initialDebt.toString()) - const repayResult = await borrowerTinlake.repay(loanId, initialDebt.toString()) + const approveTx = await borrowerTinlake.approveCurrency(contractAddresses['SHELF'], initialDebt.toString()) + await borrowerTinlake.getTransactionReceipt(approveTx) + + const repayTx = await borrowerTinlake.repay(loanId, initialDebt.toString()) + const repayResult = await borrowerTinlake.getTransactionReceipt(repayTx) + const newDebt = await borrowerTinlake.getDebt(loanId) assert.equal(newDebt.toString(), '0') @@ -98,16 +116,21 @@ describe('borrower tests', async () => { async function mintIssue(usr: string, tinlake: ITinlake) { // super user mints nft for borrower const tokenId = `${Math.floor(Math.random() * 10e15) + 1}` - await governanceTinlake.mintNFT(testConfig.nftRegistry, usr, tokenId, '234', '345', '456') + const mintTx = await governanceTinlake.mintNFT(testConfig.nftRegistry, usr, tokenId, '234', '345', '456') + await governanceTinlake.getTransactionReceipt(mintTx) // assert usr = nftOwner const nftOwner = await tinlake.getNFTOwner(testConfig.nftRegistry, tokenId) assert.equal(`${nftOwner}`.toLowerCase(), usr.toLowerCase()) - const issueResult: any = await tinlake.issue(testConfig.nftRegistry, tokenId) + const issueTx = await tinlake.issue(testConfig.nftRegistry, tokenId) + const issueResult = await tinlake.getTransactionReceipt(issueTx) + const loanId = `${(await tinlake.loanCount()).toNumber() - 1}` + // assert loan successfully issued assert.equal(issueResult.status, SUCCESS_STATUS) + // assert usr = loanOwner const titleOwner = `${await tinlake.getOwnerOfLoan(loanId)}` assert.equal(titleOwner.toLowerCase(), usr.toLowerCase()) @@ -118,25 +141,31 @@ async function mintIssue(usr: string, tinlake: ITinlake) { async function mintIssueBorrow(usr: string, tinlake: ITinlake, amount: string) { const { tokenId, loanId } = await mintIssue(usr, tinlake) - await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['NFT_FEED']); + const relyTx = await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['NFT_FEED']) + await governanceTinlake.getTransactionReceipt(relyTx) + const nftfeedId = await adminTinlake.getNftFeedId(testConfig.nftRegistry, Number(tokenId)) - await adminTinlake.updateNftFeed(nftfeedId, Number(amount)) + const updateNftTx = await adminTinlake.updateNftFeed(nftfeedId, Number(amount)) + await adminTinlake.getTransactionReceipt(updateNftTx) // approve shelf to take nft - await borrowerTinlake.approveNFT(testConfig.nftRegistry, tokenId, contractAddresses['SHELF']) + const approveTx = await borrowerTinlake.approveNFT(testConfig.nftRegistry, tokenId, contractAddresses['SHELF']) + await borrowerTinlake.getTransactionReceipt(approveTx) + // lock nft - await borrowerTinlake.lock(loanId) + const lockTx = await borrowerTinlake.lock(loanId) + await borrowerTinlake.getTransactionReceipt(lockTx) const initialBorrowerCurrencyBalance = await borrowerTinlake.getCurrencyBalance(borrowerAccount.address) + // supply tranche with money - console.log({ initialBorrowerCurrencyBalance }) - const borrowResult = await borrowerTinlake.borrow(loanId, amount) - console.log({ borrowResult }) - const withdrawResult = await borrowerTinlake.withdraw(loanId, amount, borrowerAccount.address) - console.log({ withdrawResult }) + const borrowTx = await borrowerTinlake.borrow(loanId, amount) + const borrowResult = await borrowerTinlake.getTransactionReceipt(borrowTx) + + const withdrawTx = await borrowerTinlake.withdraw(loanId, amount, borrowerAccount.address) + const withdrawResult = await borrowerTinlake.getTransactionReceipt(withdrawTx) const newBorrowerCurrencyBalance = await borrowerTinlake.getCurrencyBalance(borrowerAccount.address) - console.log({ newBorrowerCurrencyBalance }) assert.equal(initialBorrowerCurrencyBalance.add(new BN(amount)).toString(), newBorrowerCurrencyBalance.toString()) assert.equal(borrowResult.status, SUCCESS_STATUS) @@ -146,18 +175,29 @@ async function mintIssueBorrow(usr: string, tinlake: ITinlake, amount: string) { } async function fundTranche(amount: string) { - const lenderAccount = account.generate(randomString.generate(32)) + const lenderAccount = ethers.Wallet.createRandom() const lenderTinlake = createTinlake(lenderAccount, testConfig) + // fund lender accoutn with eth await testProvider.fundAccountWithETH(lenderAccount.address, FAUCET_AMOUNT) + // make admin adress ward on tranche operator - await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['JUNIOR_OPERATOR']) + const relyTx = await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['JUNIOR_OPERATOR']) + await governanceTinlake.getTransactionReceipt(relyTx) + // whitelist lender - await adminTinlake.approveAllowanceJunior(lenderAccount.address, amount, amount) + const approveAllowanceTx = await adminTinlake.approveAllowanceJunior(lenderAccount.address, amount, amount) + await adminTinlake.getTransactionReceipt(approveAllowanceTx) + // lender approves tranche to take currency - await lenderTinlake.approveCurrency(contractAddresses['JUNIOR'], amount) + const approveCurrencyTx = await lenderTinlake.approveCurrency(contractAddresses['JUNIOR_TRANCHE'], amount) + await lenderTinlake.getTransactionReceipt(approveCurrencyTx) + // mint currency for lender - await governanceTinlake.mintCurrency(lenderAccount.address, amount) + const mintTx = await governanceTinlake.mintCurrency(lenderAccount.address, amount) + await governanceTinlake.getTransactionReceipt(mintTx) + // lender supplies tranche with funds - await lenderTinlake.supplyJunior(amount) + const supplyTx = await lenderTinlake.supplyJunior(amount) + await lenderTinlake.getTransactionReceipt(supplyTx) } diff --git a/src/actions/borrower.ts b/src/actions/borrower.ts index 057ed01..d52358c 100644 --- a/src/actions/borrower.ts +++ b/src/actions/borrower.ts @@ -1,74 +1,53 @@ -import { Constructor, TinlakeParams } from '../Tinlake' -import { waitAndReturnEvents, executeAndRetry } from '../services/ethereum' +import { Constructor, TinlakeParams, PendingTransaction } from '../Tinlake' import { ethers } from 'ethers' export function BorrowerActions>(Base: ActionsBase) { return class extends Base implements IBorrowerActions { issue = async (registry: string, tokenId: string) => { - const txHash = await executeAndRetry(this.contracts['SHELF'].issue, [registry, tokenId, this.ethConfig]) - console.log(`[Issue Loan] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SHELF'].abi, this.transactionTimeout) + return this.pending(this.contract('SHELF').issue(registry, tokenId, this.overrides)) } nftLookup = async (registry: string, tokenId: string) => { const nft = ethers.utils.solidityKeccak256(['address', 'uint'], [registry, tokenId]) - console.log('NFT Look Up]') - const res = await executeAndRetry(this.contracts['SHELF'].nftlookup, [nft, this.ethConfig]) - return res[0].toString() + const loanId = await this.contract('SHELF').nftlookup(nft) + return loanId } lock = async (loan: string) => { - const txHash = await executeAndRetry(this.contracts['SHELF'].lock, [loan, this.ethConfig]) - console.log(`[Collateral NFT lock] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SHELF'].abi, this.transactionTimeout) + return this.pending(this.contract('SHELF').lock(loan, this.overrides)) } unlock = async (loan: string) => { - const txHash = await executeAndRetry(this.contracts['SHELF'].unlock, [loan, this.ethConfig]) - console.log(`[Collateral NFT unlock] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SHELF'].abi, this.transactionTimeout) + return this.pending(this.contract('SHELF').unlock(loan, this.overrides)) } close = async (loan: string) => { - const txHash = await executeAndRetry(this.contracts['SHELF'].close, [loan, this.ethConfig]) - console.log(`[Loan close] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SHELF'].abi, this.transactionTimeout) + return this.pending(this.contract('SHELF').close(loan, this.overrides)) } borrow = async (loan: string, currencyAmount: string) => { - const txHash = await executeAndRetry(this.contracts['SHELF'].borrow, [loan, currencyAmount, this.ethConfig]) - console.log(`[Borrow] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SHELF'].abi, this.transactionTimeout) + return this.pending(this.contract('SHELF').borrow(loan, currencyAmount, this.overrides)) } withdraw = async (loan: string, currencyAmount: string, usr: string) => { - const txHash = await executeAndRetry(this.contracts['SHELF'].withdraw, [ - loan, - currencyAmount, - usr, - this.ethConfig, - ]) - console.log(`[Withdraw] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SHELF'].abi, this.transactionTimeout) + return this.pending(this.contract('SHELF').withdraw(loan, currencyAmount, usr, this.overrides)) } repay = async (loan: string, currencyAmount: string) => { - const txHash = await executeAndRetry(this.contracts['SHELF'].repay, [loan, currencyAmount, this.ethConfig]) - console.log(`[Repay] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SHELF'].abi, this.transactionTimeout) + return this.pending(this.contract('SHELF').repay(loan, currencyAmount, this.overrides)) } } } export type IBorrowerActions = { - issue(registry: string, tokenId: string): Promise - nftLookup(registry: string, tokenId: string): Promise - lock(loan: string): Promise - unlock(loan: string): Promise - close(loan: string): Promise - borrow(loan: string, currencyAmount: string): Promise - withdraw(loan: string, currencyAmount: string, usr: string): Promise - repay(loan: string, currencyAmount: string): Promise + issue(registry: string, tokenId: string): Promise + nftLookup(registry: string, tokenId: string): Promise + lock(loan: string): Promise + unlock(loan: string): Promise + close(loan: string): Promise + borrow(loan: string, currencyAmount: string): Promise + withdraw(loan: string, currencyAmount: string, usr: string): Promise + repay(loan: string, currencyAmount: string): Promise } export default BorrowerActions diff --git a/src/actions/collateral.ts b/src/actions/collateral.ts index 5ab33a7..6ad3fac 100644 --- a/src/actions/collateral.ts +++ b/src/actions/collateral.ts @@ -1,85 +1,89 @@ -import { Constructor, TinlakeParams } from '../Tinlake' -import { waitAndReturnEvents, executeAndRetry } from '../services/ethereum' +import { Constructor, TinlakeParams, PendingTransaction } from '../Tinlake' import BN from 'bn.js' +const util = require('util') export function CollateralActions>(Base: ActionsBase) { return class extends Base implements ICollateralActions { - mintTitleNFT = async (nftAddr: string, user: string) => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const txHash = await executeAndRetry(nft.issue, [user, this.ethConfig]) - console.log(`[Mint NFT] txHash: ${txHash}`) - const res: any = await waitAndReturnEvents( - this.eth, - txHash, - this.contractAbis['COLLATERAL_NFT'], - this.transactionTimeout - ) - return res.events[0].data[2].toString() - } + // mintTitleNFT = async (nftAddr: string, user: string) => { + // // TODO: this is untested right now + // const collateralNft = this.contract('COLLATERAL_NFT', nftAddr) + // console.log(collateralNft) + // console.log(this.contract('COLLATERAL_NFT', nftAddr).functions) + // const tx = await collateralNft.issue(user, this.overrides) + // const receipt = await this.getTransactionReceipt(tx) + + // if (!(receipt.logs && receipt.logs[0])) { + // console.log(util.inspect(receipt, { showHidden: false, depth: null })) + // throw new Error('Event missing in COLLATERAL_NFT.issue(user) receipt') + // } + + // const parsedLog = this.contract('PROXY_REGISTRY').interface.parseLog(receipt.logs[0]) + // const nftId = parsedLog.values['2'].toString() + // return nftId + // } - mintNFT = async (nftAddr: string, owner: string, tokenId: string, ref: string, amount: string, asset: string) => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const txHash = await executeAndRetry(nft.mint, [owner, tokenId, ref, amount, asset, this.ethConfig]) - console.log(`[NFT.mint] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contractAbis['COLLATERAL_NFT'], this.transactionTimeout) + mintNFT = async ( + nftAddress: string, + owner: string, + tokenId: string, + ref: string, + amount: string, + asset: string + ) => { + const nft = this.contract('COLLATERAL_NFT', nftAddress) + return this.pending(nft.mint(owner, tokenId, ref, amount, asset, this.overrides)) } - approveNFT = async (nftAddr: string, tokenId: string, to: string) => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const txHash = await executeAndRetry(nft.approve, [to, tokenId, this.ethConfig]) - console.log(`[NFT Approve] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contractAbis['COLLATERAL_NFT'], this.transactionTimeout) + approveNFT = async (nftAddress: string, tokenId: string, to: string) => { + const nft = this.contract('COLLATERAL_NFT', nftAddress) + return this.pending(nft.approve(to, tokenId, this.overrides)) } - setNFTApprovalForAll = async (nftAddr: string, to: string, approved: boolean) => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const txHash = await executeAndRetry(nft.setApprovalForAll, [to, approved, this.ethConfig]) - return waitAndReturnEvents(this.eth, txHash, this.contractAbis['COLLATERAL_NFT'], this.transactionTimeout) + setNFTApprovalForAll = async (nftAddress: string, to: string, approved: boolean) => { + const nft = this.contract('COLLATERAL_NFT', nftAddress) + return this.pending(nft.setApprovalForAll(to, approved, this.overrides)) } - isNFTApprovedForAll = async (nftAddr: string, owner: string, operator: string) => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const res: { 0: boolean } = await executeAndRetry(nft.isApprovedForAll, [owner, operator, this.ethConfig]) - return res[0] + isNFTApprovedForAll = async (nftAddress: string, owner: string, operator: string) => { + return this.contract('COLLATERAL_NFT', nftAddress).isApprovedForAll(owner, operator) } - getNFTCount = async (nftAddr: string): Promise => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const res: { 0: BN } = await executeAndRetry(nft.count, []) - return res[0] + getNFTCount = async (nftAddress: string): Promise => { + return (await this.contract('COLLATERAL_NFT', nftAddress).count()).toBN() } - getNFTData = async (nftAddr: string, tokenId: string): Promise => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const res = await executeAndRetry(nft.data, [tokenId]) - return res + getNFTData = async (nftAddress: string, tokenId: string): Promise => { + return this.contract('COLLATERAL_NFT', nftAddress).data(tokenId) } - getNFTOwner = async (nftAddr: string, tokenId: string): Promise => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const res: { 0: BN } = await executeAndRetry(nft.ownerOf, [tokenId]) - return res[0] + getNFTOwner = async (nftAddresss: string, tokenId: string): Promise => { + return this.contract('COLLATERAL_NFT', nftAddresss).ownerOf(tokenId) } - transferNFT = async (nftAddr: string, from: string, to: string, tokenId: string) => { - const nft: any = this.eth.contract(this.contractAbis['COLLATERAL_NFT']).at(nftAddr) - const txHash = await executeAndRetry(nft.transferFrom, [from, to, tokenId, this.ethConfig]) - console.log(`[NFT Approve] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contractAbis['COLLATERAL_NFT'], this.transactionTimeout) + transferNFT = async (nftAddress: string, from: string, to: string, tokenId: string) => { + const nft = this.contract('COLLATERAL_NFT', nftAddress) + return this.pending(nft.transferFrom(from, to, tokenId, this.overrides)) } } } export type ICollateralActions = { - mintTitleNFT(nftAddr: string, usr: string): Promise - mintNFT(nftAddr: string, owner: string, tokenId: string, ref: string, amount: string, asset: string): Promise - approveNFT(nftAddr: string, tokenId: string, to: string): Promise - setNFTApprovalForAll(nftAddr: string, to: string, approved: boolean): Promise + // mintTitleNFT(nftAddr: string, usr: string): Promise + mintNFT( + nftAddr: string, + owner: string, + tokenId: string, + ref: string, + amount: string, + asset: string + ): Promise + approveNFT(nftAddr: string, tokenId: string, to: string): Promise + setNFTApprovalForAll(nftAddr: string, to: string, approved: boolean): Promise isNFTApprovedForAll(nftAddr: string, owner: string, operator: string): Promise getNFTCount(nftAddr: string): Promise getNFTData(nftAddr: string, tokenId: string): Promise - getNFTOwner(nftAddr: string, tokenId: string): Promise - transferNFT(nftAddr: string, from: string, to: string, tokenId: string): Promise + getNFTOwner(nftAddr: string, tokenId: string): Promise + transferNFT(nftAddr: string, from: string, to: string, tokenId: string): Promise } export default CollateralActions diff --git a/src/actions/coordinator.ts b/src/actions/coordinator.ts index 5dc3125..a16d59e 100644 --- a/src/actions/coordinator.ts +++ b/src/actions/coordinator.ts @@ -2,29 +2,54 @@ import { Constructor, TinlakeParams } from '../Tinlake' export function CoordinatorActions>(Base: ActionsBase) { return class extends Base implements ICoordinatorActions { + // const tinlake = (this as any) + // const reserve = (await tinlake.getJuniorReserve()).add(await tinlake.getSeniorReserve()) + solveEpoch = async () => { - // const tinlake = (this as any) - // const reserve = (await tinlake.getJuniorReserve()).add(await tinlake.getSeniorReserve()) + // if (!coordinator.submissionPeriod) { + // await coordinator.closeEpoch() + + // if (!coordinator.submissionPeriod) return + // } // const state = { - // reserve, - // netAssetValue: 0, - // seniorDebt: await tinlake.getSeniorDebt(), - // seniorBalance: 0, - // minTinRatio: await tinlake.getMinJuniorRatio(), - // maxTinRatio: 0, - // maxReserve: 0, + // reserve, // coordinator.epochReserve + // netAssetValue: 0, // coordinator.epochNAV + // seniorDebt: await tinlake.getSeniorDebt(), // coordinator.epochSeniorDebt (to be added) + // seniorBalance: 0, // epochSeniorAsset - epochSeniorDebt + // minTinRatio: await tinlake.getMinJuniorRatio(), // 1 - maxSeniorRatio on the assessor + // maxTinRatio: 0, // 1 - mSeniorRatio on the assessor + // maxReserve: 0, // assessor.maxReserve // } - + + // const orderState = coordinator.order + + // const solution = calculateOptimalSolution(state, orderState) + + // Call submitSolution(solution) + return Promise.resolve({ - tinRedeem: 1, - dropRedeem: 2, - tinInvest: 3, - dropInvest: 4 + tinRedeem: 1, + dropRedeem: 2, + tinInvest: 3, + dropInvest: 4, }) } + // executeEpoch = () => void + + // isInChallengePeriod = () => boolean + // check coordinator.minChallengePeriodEnd + calculateOptimalSolution = async (state: State, orderState: OrderState) => { + /** + * The limitations are: + * - only input variables (those in state or orderState) can be on the right side of the constraint (the bnds key) + * - only output variables ([dropRedeem,tinRedeem,tinInvest,dropInvest]) can be on the left side of the constraint (the vars key) + * - variables can have coefficients, but there's no option for brackets or other more advanced equation forms + * (e.g. it's limited to a * x_1 + b * x_2 + ..., where [a,b] are coefficients and [x_1,x_2] are variables) + * - larger than or equals, less than or equals, and equals constraints are all allowed ([<=,>=,=]) + */ return require('glpk.js').then((glpk: any) => { const lp = { name: 'LP', @@ -82,10 +107,10 @@ export function CoordinatorActions>(Base: ActionsBase) { return class extends Base implements ICurrencyActions { // move out for tests only mintCurrency = async (usr: string, amount: string) => { - const txHash = await executeAndRetry(this.contracts['TINLAKE_CURRENCY'].mint, [usr, amount, this.ethConfig]) - console.log(`[Mint currency] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['TINLAKE_CURRENCY'].abi, this.transactionTimeout) + return this.pending(this.contract('TINLAKE_CURRENCY').mint(usr, amount, this.overrides)) } getCurrencyAllowance = async (owner: string, spender: string) => { - const res: { 0: BN } = await executeAndRetry(this.contracts['TINLAKE_CURRENCY'].allowance, [owner, spender]) - return res[0] || new BN(0) + const currencyContract = this.contract('TINLAKE_CURRENCY') + return (await currencyContract.allowance(owner, spender)).toBN() } getJuniorForCurrencyAllowance = async (owner: string) => { - if (!this.contractAddresses['JUNIOR']) return - return this.getCurrencyAllowance(owner, this.contractAddresses['JUNIOR']) + if (!this.contractAddresses['JUNIOR_TRANCHE']) return + return this.getCurrencyAllowance(owner, this.contractAddresses['JUNIOR_TRANCHE']) } getSeniorForCurrencyAllowance = async (owner: string) => { - if (!this.contractAddresses['SENIOR']) return - return this.getCurrencyAllowance(owner, this.contractAddresses['SENIOR']) + if (!this.contractAddresses['SENIOR_TRANCHE']) return + return this.getCurrencyAllowance(owner, this.contractAddresses['SENIOR_TRANCHE']) } getCurrencyBalance = async (user: string) => { - const res: { 0: BN } = await executeAndRetry(this.contracts['TINLAKE_CURRENCY'].balanceOf, [user]) - return res[0] || new BN(0) + return (await this.contract('TINLAKE_CURRENCY').balanceOf(user)).toBN() } approveCurrency = async (usr: string, currencyAmount: string) => { - const txHash = await executeAndRetry(this.contracts['TINLAKE_CURRENCY'].approve, [ - usr, - currencyAmount, - this.ethConfig, - ]) - console.log(`[Currency.approve] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['TINLAKE_CURRENCY'].abi, this.transactionTimeout) + const currencyContract = this.contract('TINLAKE_CURRENCY') + return this.pending(currencyContract.approve(usr, currencyAmount, this.overrides)) } approveSeniorForCurrency = async (currencyAmount: string) => { - if (!this.contractAddresses['SENIOR']) return - return this.approveCurrency(this.contractAddresses['SENIOR'], currencyAmount) + if (!this.contractAddresses['SENIOR_TRANCHE']) return + return this.approveCurrency(this.contractAddresses['SENIOR_TRANCHE'], currencyAmount) } approveJuniorForCurrency = async (currencyAmount: string) => { - if (!this.contractAddresses['JUNIOR']) return - return this.approveCurrency(this.contractAddresses['JUNIOR'], currencyAmount) + if (!this.contractAddresses['JUNIOR_TRANCHE']) return + return this.approveCurrency(this.contractAddresses['JUNIOR_TRANCHE'], currencyAmount) } } } export type ICurrencyActions = { - mintCurrency(usr: string, amount: string): Promise + mintCurrency(usr: string, amount: string): Promise getCurrencyBalance(usr: string): Promise - approveCurrency(usr: string, amount: string): Promise getCurrencyAllowance: (owner: string, spender: string) => Promise getJuniorForCurrencyAllowance: (owner: string) => Promise getSeniorForCurrencyAllowance: (owner: string) => Promise - approveSeniorForCurrency: (currencyAmount: string) => Promise - approveJuniorForCurrency: (currencyAmount: string) => Promise + approveCurrency(usr: string, amount: string): Promise + approveSeniorForCurrency: (currencyAmount: string) => Promise + approveJuniorForCurrency: (currencyAmount: string) => Promise } export default CurrencyActions diff --git a/src/actions/governance.spec.ts b/src/actions/governance.spec.ts index 9c93775..bb98856 100644 --- a/src/actions/governance.spec.ts +++ b/src/actions/governance.spec.ts @@ -1,14 +1,13 @@ -const randomString = require('randomstring') -const account = require('ethjs-account') import assert from 'assert' import { ITinlake } from '../types/tinlake' import { createTinlake, TestProvider } from '../test/utils' import testConfig from '../test/config' import { Account } from '../test/types' -import { ContractNames } from '../Tinlake' +import { ethers } from 'ethers' +import { ContractName } from '../Tinlake' // god account = governance address for the tinlake test deployment -const userAccount = account.generate(randomString.generate(32)) +const userAccount = ethers.Wallet.createRandom() let governanceTinlake: ITinlake const { SUCCESS_STATUS, FAIL_STATUS, FAUCET_AMOUNT } = testConfig @@ -35,20 +34,26 @@ describe('governance tests', async () => { }) it('fail: account has no governance permissions', async () => { - const randomAccount: Account = account.generate(randomString.generate(32)) + const randomAccount = ethers.Wallet.createRandom() const testProvider = new TestProvider(testConfig) await testProvider.fundAccountWithETH(randomAccount.address, FAUCET_AMOUNT) const randomTinlake = createTinlake(randomAccount, testConfig) - const res = await randomTinlake.relyAddress(userAccount.address, testConfig.contractAddresses['PILE']) + + const tx = await randomTinlake.relyAddress(userAccount.address, testConfig.contractAddresses['PILE']) + const res = await randomTinlake.getTransactionReceipt(tx) + assert.equal(res.status, FAIL_STATUS) }) }) }) -async function relyAddress(usr: string, contractName: ContractNames) { - const res = await governanceTinlake.relyAddress(usr, testConfig.contractAddresses[contractName]) +async function relyAddress(usr: string, contractName: ContractName) { + const tx = await governanceTinlake.relyAddress(usr, testConfig.contractAddresses[contractName]) + const res = await governanceTinlake.getTransactionReceipt(tx) + const isWard = await governanceTinlake.isWard(usr, contractName) + assert.equal(isWard, 1) assert.equal(res.status, SUCCESS_STATUS) } diff --git a/src/actions/governance.ts b/src/actions/governance.ts index 1eee581..2fb4c9b 100644 --- a/src/actions/governance.ts +++ b/src/actions/governance.ts @@ -1,19 +1,17 @@ -import { Constructor, TinlakeParams } from '../Tinlake' -import { executeAndRetry, waitAndReturnEvents } from '../services/ethereum' +import { Constructor, TinlakeParams, PendingTransaction } from '../Tinlake' function GovernanceActions>(Base: ActionsBase) { return class extends Base implements IGovernanceActions { relyAddress = async (usr: string, contractAddress: string) => { - const rootContract = this.contracts['ROOT_CONTRACT'] - const txHash = await executeAndRetry(rootContract.relyContract, [contractAddress, usr, this.ethConfig]) - console.log(`[Rely usr ${usr}] txHash: ${txHash} on contract ${contractAddress}`) - return waitAndReturnEvents(this.eth, txHash, rootContract.abi, this.transactionTimeout) + const contract = this.contract('ROOT_CONTRACT') + const tx = contract.relyContract(contractAddress, usr, this.overrides) + return this.pending(tx) } } } export type IGovernanceActions = { - relyAddress(usr: string, contractAddress: string): Promise + relyAddress(usr: string, contractAddress: string): Promise } export default GovernanceActions diff --git a/src/actions/index.ts b/src/actions/index.ts index 340ac88..94523db 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -17,7 +17,7 @@ export default { Analytics, Governance, Proxy, - Coordinator + Coordinator, } export type TinlakeActions = IAdminActions & diff --git a/src/actions/lender.spec.ts b/src/actions/lender.spec.ts index 95601a6..41fe204 100644 --- a/src/actions/lender.spec.ts +++ b/src/actions/lender.spec.ts @@ -1,7 +1,5 @@ import assert from 'assert' -const account = require('ethjs-account') -const randomString = require('randomstring') - +import { ethers } from 'ethers' import testConfig from '../test/config' import { ITinlake } from '../types/tinlake' import { createTinlake, TestProvider } from '../test/utils' @@ -10,7 +8,7 @@ import BN from 'bn.js' let lenderAccount let lenderTinlake: ITinlake -const adminAccount = account.generate(randomString.generate(32)) +const adminAccount = ethers.Wallet.createRandom() let adminTinlake: ITinlake let governanceTinlake: ITinlake const testProvider = new TestProvider(testConfig) @@ -21,106 +19,140 @@ describe('lender functions', async () => { before(async () => { adminTinlake = createTinlake(adminAccount, testConfig) governanceTinlake = createTinlake(testConfig.godAccount, testConfig) + // fund lender & admin accounts with currency await testProvider.fundAccountWithETH(adminAccount.address, FAUCET_AMOUNT) + // rely admin on junior operator - await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['JUNIOR_OPERATOR']) + const relyTx = await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['JUNIOR_OPERATOR']) + await governanceTinlake.getTransactionReceipt(relyTx) }) beforeEach(async () => { - lenderAccount = account.generate(randomString.generate(32)) + lenderAccount = ethers.Wallet.createRandom() lenderTinlake = createTinlake(lenderAccount, testConfig) - await testProvider.fundAccountWithETH(lenderAccount.address, FAUCET_AMOUNT) + return await testProvider.fundAccountWithETH(lenderAccount.address, FAUCET_AMOUNT) }) it('success: supply junior', async () => { - const currencyAmount = '100000' - const tokenAmount = '100' + const currencyAmount = '1000' + const tokenAmount = '1' + // whitelist investor - await adminTinlake.approveAllowanceJunior(lenderAccount.address, currencyAmount, tokenAmount) + const approveTx = await adminTinlake.approveAllowanceJunior(lenderAccount.address, currencyAmount, tokenAmount) + await adminTinlake.getTransactionReceipt(approveTx) + await supply(lenderAccount.address, `${currencyAmount}`, lenderTinlake) }) it('fail: supply junior - no allowance', async () => { - const currencyAmount = '1000' + const currencyAmount = '10' // approve junior tranche to take currency - await lenderTinlake.approveCurrency(contractAddresses['JUNIOR'], currencyAmount) + const approveTx = await lenderTinlake.approveCurrency(contractAddresses['JUNIOR_TRANCHE'], currencyAmount) + const approval = await lenderTinlake.getTransactionReceipt(approveTx) + // console.log('approval', approval) + // fund investor with tinlake currency - await governanceTinlake.mintCurrency(lenderAccount.address, currencyAmount) + const mintTx = await governanceTinlake.mintCurrency(lenderAccount.address, currencyAmount) + const mint = await governanceTinlake.getTransactionReceipt(mintTx) + // console.log('mint', mint) // do not set allowance for lender - const supplyResult = await lenderTinlake.supplyJunior(currencyAmount) + const supplyTx = await lenderTinlake.supplyJunior(currencyAmount) + const supplyResult = await lenderTinlake.getTransactionReceipt(supplyTx) - // assert result successful + // assert result failed assert.equal(supplyResult.status, FAIL_STATUS) }) it('success: redeem junior', async () => { const currencyAmount = '10000' const tokenAmount = '100' + // whitelist investor - await adminTinlake.approveAllowanceJunior(lenderAccount.address, currencyAmount, tokenAmount) + const approveTx = await adminTinlake.approveAllowanceJunior(lenderAccount.address, currencyAmount, tokenAmount) + await adminTinlake.getTransactionReceipt(approveTx) + // supply currency - receive tokens await supply(lenderAccount.address, `${currencyAmount}`, lenderTinlake) + // approve junior tranche to take tokens - await lenderTinlake.approveJuniorToken(tokenAmount) + const lenderApproveTx = await lenderTinlake.approveJuniorToken(tokenAmount) + await lenderTinlake.getTransactionReceipt(lenderApproveTx) const initialLenderCurrencyBalance: BN = await lenderTinlake.getCurrencyBalance(lenderAccount.address) - const initialTrancheCurrencyBalance: BN = await lenderTinlake.getCurrencyBalance(contractAddresses['JUNIOR']) + const initialTrancheCurrencyBalance: BN = await lenderTinlake.getCurrencyBalance( + contractAddresses['JUNIOR_TRANCHE'] + ) const initialJuniorTokenBalance = await lenderTinlake.getJuniorTokenBalance(lenderAccount.address) - const redeemResult = await lenderTinlake.redeemJunior(tokenAmount) + const redeemTx = await lenderTinlake.redeemJunior(tokenAmount) + const redeemResult = await lenderTinlake.getTransactionReceipt(redeemTx) - const newTrancheCurrencyBalance = await lenderTinlake.getCurrencyBalance(contractAddresses['JUNIOR']) + const newTrancheCurrencyBalance = await lenderTinlake.getCurrencyBalance(contractAddresses['JUNIOR_TRANCHE']) const newLenderCurrencyBalance = await lenderTinlake.getCurrencyBalance(lenderAccount.address) const newJuniorTokenBalance = await lenderTinlake.getJuniorTokenBalance(lenderAccount.address) + assert.equal(redeemResult.status, SUCCESS_STATUS) assert.equal( - initialTrancheCurrencyBalance.sub(new BN(tokenAmount)).subn(1).toString(), + initialTrancheCurrencyBalance.sub(new BN(tokenAmount)).toString(), newTrancheCurrencyBalance.toString() ) - assert.equal( - initialLenderCurrencyBalance.add(new BN(tokenAmount)).addn(1).toString(), - newLenderCurrencyBalance.toString() - ) + assert.equal(initialLenderCurrencyBalance.add(new BN(tokenAmount)).toString(), newLenderCurrencyBalance.toString()) assert.equal(tokenAmount, initialJuniorTokenBalance.sub(newJuniorTokenBalance).toString()) }) it('fail: redeem junior - no allowance', async () => { - const currencyAmount = '1000' - const tokenAmount = '100' + const currencyAmount = '10' + const tokenAmount = '1' // whitelist investor with no allowance to redeem - await adminTinlake.approveAllowanceJunior(lenderAccount.address, currencyAmount, '0') + const approveTx = await adminTinlake.approveAllowanceJunior(lenderAccount.address, currencyAmount, '0') + await adminTinlake.getTransactionReceipt(approveTx) + // supply currency - receive tokens await supply(lenderAccount.address, `${currencyAmount}`, lenderTinlake) + // approve junior tranche to take tokens - await lenderTinlake.approveJuniorToken(tokenAmount) - const redeemResult = await lenderTinlake.redeemJunior(tokenAmount) + const lenderApproveTx = await lenderTinlake.approveJuniorToken(tokenAmount) + await lenderTinlake.getTransactionReceipt(lenderApproveTx) + + const redeemTx = await lenderTinlake.redeemJunior(tokenAmount) + const redeemResult = await lenderTinlake.getTransactionReceipt(redeemTx) + assert.equal(redeemResult.status, FAIL_STATUS) }) }) async function supply(investor: string, currencyAmount: string, tinlake: ITinlake) { // approve junior tranche to take currency - await tinlake.approveCurrency(contractAddresses['JUNIOR'], currencyAmount) + const approveTx = await tinlake.approveCurrency(contractAddresses['JUNIOR_TRANCHE'], currencyAmount) + await tinlake.getTransactionReceipt(approveTx) + // fund investor with tinlake currency - const res = await governanceTinlake.mintCurrency(investor, currencyAmount) + const mintTx = await governanceTinlake.mintCurrency(investor, currencyAmount) + await governanceTinlake.getTransactionReceipt(mintTx) + const initialLenderCurrencyBalance = await tinlake.getCurrencyBalance(investor) - const initialTrancheCurrencyBalance = await tinlake.getCurrencyBalance(contractAddresses['JUNIOR']) + const initialTrancheCurrencyBalance = await tinlake.getCurrencyBalance(contractAddresses['JUNIOR_TRANCHE']) const initialJuniorTokenBalance = await tinlake.getJuniorTokenBalance(investor) - const supplyResult = await tinlake.supplyJunior(currencyAmount) - const newTrancheCurrencyBalance = await tinlake.getCurrencyBalance(contractAddresses['JUNIOR']) + const supplyTx = await tinlake.supplyJunior(currencyAmount) + const supplyResult = await tinlake.getTransactionReceipt(supplyTx) + + const newTrancheCurrencyBalance = await tinlake.getCurrencyBalance(contractAddresses['JUNIOR_TRANCHE']) const newLenderCurrencyBalance = await tinlake.getCurrencyBalance(investor) const newJuniorTokenBalance = await tinlake.getJuniorTokenBalance(investor) // assert result successful assert.equal(supplyResult.status, SUCCESS_STATUS) + // assert tranche balance increased by currency amount assert.equal(newTrancheCurrencyBalance.sub(initialTrancheCurrencyBalance).toString(), currencyAmount) + // assert investor currency balanace decreased assert.equal(initialLenderCurrencyBalance.sub(newLenderCurrencyBalance).toString(), currencyAmount) + // assert investor received tokens if (testConfig.isRealTestnet) { assert.ok(newJuniorTokenBalance.gt(initialJuniorTokenBalance)) diff --git a/src/actions/lender.ts b/src/actions/lender.ts index 701f203..d0ca7b6 100644 --- a/src/actions/lender.ts +++ b/src/actions/lender.ts @@ -1,76 +1,53 @@ -import { Constructor, TinlakeParams } from '../Tinlake' -import { executeAndRetry, waitAndReturnEvents } from '../services/ethereum' +import { Constructor, TinlakeParams, PendingTransaction } from '../Tinlake' import BN from 'bn.js' export function LenderActions>(Base: ActionBase) { return class extends Base implements ILenderActions { - // senior tranch functions + // senior tranche functions supplySenior = async (currencyAmount: string) => { - const txHash = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].supply, [currencyAmount, this.ethConfig]) - console.log(`[Supply] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SENIOR_OPERATOR'].abi, this.transactionTimeout) + return this.pending(this.contract('SENIOR_OPERATOR').supply(currencyAmount, this.overrides)) } redeemSenior = async (tokenAmount: string) => { - const txHash = await executeAndRetry(this.contracts['SENIOR_OPERATOR'].redeem, [tokenAmount, this.ethConfig]) - console.log(`[Redeem] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SENIOR_OPERATOR'].abi, this.transactionTimeout) + return this.pending(this.contract('SENIOR_OPERATOR').redeem(tokenAmount, this.overrides)) } getSeniorTokenAllowance = async (owner: string) => { - const res: { 0: BN } = await executeAndRetry(this.contracts['SENIOR_TOKEN'].allowance, [ - owner, - this.contractAddresses['SENIOR'], - ]) - return res[0] || new BN(0) + return ( + await this.contract('SENIOR_TOKEN').allowance(owner, this.contractAddresses['SENIOR_TRANCHE'], this.overrides) + ).toBN() } approveSeniorToken = async (tokenAmount: string) => { - const txHash = await executeAndRetry(this.contracts['SENIOR_TOKEN'].approve, [ - this.contractAddresses['SENIOR'], - tokenAmount, - this.ethConfig, - ]) - console.log(`[Currency.approve] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['SENIOR_TOKEN'].abi, this.transactionTimeout) + return this.pending( + this.contract('SENIOR_TOKEN').approve(this.contractAddresses['SENIOR_TRANCHE'], tokenAmount, this.overrides) + ) } // junior tranche functions supplyJunior = async (currencyAmount: string) => { - const txHash = await executeAndRetry(this.contracts['JUNIOR_OPERATOR'].supply, [currencyAmount, this.ethConfig]) - console.log(`[Supply] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['JUNIOR_OPERATOR'].abi, this.transactionTimeout) + return this.pending(this.contract('JUNIOR_OPERATOR').supply(currencyAmount, this.overrides)) } redeemJunior = async (tokenAmount: string) => { - const txHash = await executeAndRetry(this.contracts['JUNIOR_OPERATOR'].redeem, [tokenAmount, this.ethConfig]) - console.log(`[Redeem] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['JUNIOR_OPERATOR'].abi, this.transactionTimeout) + return this.pending(this.contract('JUNIOR_OPERATOR').redeem(tokenAmount, this.overrides)) } getJuniorTokenAllowance = async (owner: string) => { - const res: { 0: BN } = await executeAndRetry(this.contracts['JUNIOR_TOKEN'].allowance, [ - owner, - this.contractAddresses['JUNIOR'], - ]) - return res[0] || new BN(0) + return ( + await this.contract('JUNIOR_TOKEN').allowance(owner, this.contractAddresses['JUNIOR_TRANCHE'], this.overrides) + ).toBN() } approveJuniorToken = async (tokenAmount: string) => { - const txHash = await executeAndRetry(this.contracts['JUNIOR_TOKEN'].approve, [ - this.contractAddresses['JUNIOR'], - tokenAmount, - this.ethConfig, - ]) - console.log(`[Currency.approve] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['JUNIOR_TOKEN'].abi, this.transactionTimeout) + return this.pending( + this.contract('JUNIOR_TOKEN').approve(this.contractAddresses['JUNIOR_TRANCHE'], tokenAmount, this.overrides) + ) } // general lender functions balance = async () => { - const txHash = await executeAndRetry(this.contracts['DISTRIBUTOR'].balance, [this.ethConfig]) - console.log(`[Balance] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contracts['DISTRIBUTOR'].abi, this.transactionTimeout) + return this.pending(this.contract('DISTRIBUTOR').balance(this.overrides)) } } } @@ -78,13 +55,13 @@ export function LenderActions>(Bas export type ILenderActions = { getSeniorTokenAllowance(owner: string): Promise getJuniorTokenAllowance(owner: string): Promise - supplyJunior(currencyAmount: string): Promise - approveJuniorToken: (tokenAmount: string) => Promise - approveSeniorToken: (tokenAmount: string) => Promise - redeemJunior(tokenAmount: string): Promise - supplySenior(currencyAmount: string): Promise - redeemSenior(tokenAmount: string): Promise - balance(): Promise + supplyJunior(currencyAmount: string): Promise + approveJuniorToken: (tokenAmount: string) => Promise + approveSeniorToken: (tokenAmount: string) => Promise + redeemJunior(tokenAmount: string): Promise + supplySenior(currencyAmount: string): Promise + redeemSenior(tokenAmount: string): Promise + balance(): Promise } export default LenderActions diff --git a/src/actions/proxy.spec.ts b/src/actions/proxy.spec.ts index 9865919..bae42bd 100644 --- a/src/actions/proxy.spec.ts +++ b/src/actions/proxy.spec.ts @@ -1,16 +1,13 @@ -const randomString = require('randomstring') -const account = require('ethjs-account') import assert from 'assert' import { createTinlake, TestProvider } from '../test/utils' import testConfig from '../test/config' -import { ethers } from 'ethers' import { ITinlake } from '../types/tinlake' -import { EthConfig } from '../Tinlake' -import BN from 'bn.js' +import { ethers } from 'ethers' +import BN from 'BN.js' const testProvider = new TestProvider(testConfig) -const borrowerAccount = account.generate(randomString.generate(32)) -const adminAccount = account.generate(randomString.generate(32)) +const borrowerAccount = ethers.Wallet.createRandom() +const adminAccount = ethers.Wallet.createRandom() let borrowerTinlake: ITinlake let adminTinlake: ITinlake let governanceTinlake: ITinlake @@ -22,6 +19,7 @@ describe('proxy tests', async () => { borrowerTinlake = createTinlake(borrowerAccount, testConfig) adminTinlake = createTinlake(adminAccount, testConfig) governanceTinlake = createTinlake(testConfig.godAccount, testConfig) + // fund accounts with ETH await testProvider.fundAccountWithETH(adminAccount.address, FAUCET_AMOUNT) await testProvider.fundAccountWithETH(borrowerAccount.address, FAUCET_AMOUNT) @@ -32,93 +30,177 @@ describe('proxy tests', async () => { // create new proxy and mint collateral NFT to borrower const proxyAddr = await borrowerTinlake.proxyCreateNew(borrowerAccount.address) const tokenId = `${Math.floor(Math.random() * 10e15) + 1}` - await governanceTinlake.mintNFT(testConfig.nftRegistry, borrowerAccount.address, tokenId, '234', '345', '456') - await borrowerTinlake.approveNFT(testConfig.nftRegistry, tokenId, proxyAddr) + + const mintTx = await governanceTinlake.mintNFT( + testConfig.nftRegistry, + borrowerAccount.address, + tokenId, + '234', + '345', + '456' + ) + const mintResult = await governanceTinlake.getTransactionReceipt(mintTx) + + // console.log('mintResult', mintResult) + + const approveTx = await borrowerTinlake.approveNFT(testConfig.nftRegistry, tokenId, proxyAddr) + const approveResult = await borrowerTinlake.getTransactionReceipt(approveTx) + + // console.log('approveResult', approveResult) + // issue loan from collateral NFT - const issueResult = await borrowerTinlake.proxyTransferIssue(proxyAddr, testConfig.nftRegistry, tokenId) + const issueTx = await borrowerTinlake.proxyTransferIssue(proxyAddr, testConfig.nftRegistry, tokenId) + const issueResult = await borrowerTinlake.getTransactionReceipt(issueTx) + + // console.log('issueResult', issueResult) + assert.equal(issueResult.status, SUCCESS_STATUS) assert.equal(await borrowerTinlake.getNFTOwner(testConfig.nftRegistry, tokenId), proxyAddr) + // set loan parameters and fund tranche - const loanId = await borrowerTinlake.nftLookup(testConfig.nftRegistry, tokenId); - const amount = '1000'; - await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['NFT_FEED']); + const loanId = await borrowerTinlake.nftLookup(testConfig.nftRegistry, tokenId) + const amount = '10' + const relyTx = await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['NFT_FEED']) + const relyResult = await governanceTinlake.getTransactionReceipt(relyTx) + + // console.log('relyResult', relyResult) + const nftfeedId = await adminTinlake.getNftFeedId(testConfig.nftRegistry, Number(tokenId)) - await adminTinlake.updateNftFeed(nftfeedId, Number(amount)) - await fundTranche('10000'); - const initialTrancheBalance = await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR']); + const updateNftTx = await adminTinlake.updateNftFeed(nftfeedId, Number(amount)) + const updateNftResult = await adminTinlake.getTransactionReceipt(updateNftTx) + + // console.log('updateNftResult', updateNftResult) + + await fundTranche('100') + const initialTrancheBalance = await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR_TRANCHE']) + // borrow - console.log(initialTrancheBalance.toNumber()) - const borrowResult = await borrowerTinlake.proxyLockBorrowWithdraw(proxyAddr, loanId, amount, borrowerAccount.address); - const balance = await borrowerTinlake.getCurrencyBalance(borrowerAccount.address); - const secondTrancheBalance = await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR']); - assert.equal(borrowResult.status, SUCCESS_STATUS); - assert.equal(balance.toString(), amount); - assert.equal(secondTrancheBalance.toString(), initialTrancheBalance.sub(new BN(amount)).toString()); + const borrowTx = await borrowerTinlake.proxyLockBorrowWithdraw(proxyAddr, loanId, amount, borrowerAccount.address) + // console.log('borrowTx', borrowTx) + + const borrowResult = await borrowerTinlake.getTransactionReceipt(borrowTx) + + // console.log('borrowResult', borrowResult) + + const balance = await borrowerTinlake.getCurrencyBalance(borrowerAccount.address) + const secondTrancheBalance = await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR_TRANCHE']) + + assert.equal(borrowResult.status, SUCCESS_STATUS) + assert.equal(balance.toString(), amount) + assert.equal(secondTrancheBalance.toString(), initialTrancheBalance.sub(new BN(amount)).toString()) + // fuel borrower with extra to cover loan interest, approve borrower proxy to take currency - await governanceTinlake.mintCurrency(borrowerAccount.address, amount.toString()); - await borrowerTinlake.approveCurrency(proxyAddr, amount.toString()); + const secondMintTx = await governanceTinlake.mintCurrency(borrowerAccount.address, amount.toString()) + await governanceTinlake.getTransactionReceipt(secondMintTx) + + const secondApproveTx = await borrowerTinlake.approveCurrency(proxyAddr, amount.toString()) + await borrowerTinlake.getTransactionReceipt(secondApproveTx) + // repay - const repayResult = await borrowerTinlake.proxyRepayUnlockClose(proxyAddr, tokenId, loanId, testConfig.nftRegistry); - assert.equal(repayResult.status, SUCCESS_STATUS); + const repayTx = await borrowerTinlake.proxyRepayUnlockClose(proxyAddr, tokenId, loanId, testConfig.nftRegistry) + const repayResult = await borrowerTinlake.getTransactionReceipt(repayTx) + assert.equal(repayResult.status, SUCCESS_STATUS) + // borrower should be owner of collateral NFT again // tranche balance should be back to pre-borrow amount - const owner = await governanceTinlake.getNFTOwner(testConfig.nftRegistry, tokenId); - assert.equal(ethers.utils.getAddress(owner.toString()), ethers.utils.getAddress(borrowerAccount.address)); - await borrowerTinlake.getCurrencyBalance(proxyAddr); - const finalTrancheBalance = await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR']); - assert.equal(initialTrancheBalance.toString(), finalTrancheBalance.toString()); + const owner = await governanceTinlake.getNFTOwner(testConfig.nftRegistry, tokenId) + assert.equal(ethers.utils.getAddress(owner.toString()), ethers.utils.getAddress(borrowerAccount.address)) + + await borrowerTinlake.getCurrencyBalance(proxyAddr) + const finalTrancheBalance = await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR_TRANCHE']) + + assert.equal(initialTrancheBalance.toString(), finalTrancheBalance.toString()) }) it('fail: does not succeed if the proxy is not approved to take the NFT', async () => { const proxyAddr = await borrowerTinlake.proxyCreateNew(borrowerAccount.address) const tokenId = `${Math.floor(Math.random() * 10e15) + 1}` - await governanceTinlake.mintNFT(testConfig.nftRegistry, borrowerAccount.address, tokenId, '234', '345', '456') - const res = await borrowerTinlake.proxyTransferIssue(testConfig.nftRegistry, proxyAddr, tokenId) - assert.equal(res.status, FAIL_STATUS) - }) - it('fail: does not succeed if the proxy is not approved to transfer currency from the borrower', async () => { - // create new proxy and mint collateral NFT to borrower - const proxyAddr = await borrowerTinlake.proxyCreateNew(borrowerAccount.address); - const nftId: any = await governanceTinlake.mintTitleNFT(testConfig.nftRegistry, borrowerAccount.address); - await borrowerTinlake.approveNFT(testConfig.nftRegistry, nftId, proxyAddr); - // issue loan from collateral NFT - await borrowerTinlake.proxyTransferIssue(proxyAddr, testConfig.nftRegistry, nftId); - // set loan parameters and fund tranche - const loanId = await borrowerTinlake.nftLookup(testConfig.nftRegistry, nftId); - const amount = 1000; - await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['NFT_FEED']); - const nftfeedId = await adminTinlake.getNftFeedId(testConfig.nftRegistry, Number(nftId)) - await adminTinlake.updateNftFeed(nftfeedId, Number(amount)) - await fundTranche('1000000000'); - await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR']); - // borrow - await borrowerTinlake.proxyLockBorrowWithdraw(proxyAddr, loanId, amount.toString(), borrowerAccount.address); - await borrowerTinlake.getCurrencyBalance(borrowerAccount.address); - await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR']); - // does not approve proxy to transfer currency - await governanceTinlake.mintCurrency(borrowerAccount.address, amount.toString()); - // repay - const repayResult = await borrowerTinlake.proxyRepayUnlockClose(proxyAddr, nftId, loanId, testConfig.nftRegistry); - assert.equal(repayResult.status, FAIL_STATUS); + const mintTx = await governanceTinlake.mintNFT( + testConfig.nftRegistry, + borrowerAccount.address, + tokenId, + '234', + '345', + '456' + ) + await governanceTinlake.getTransactionReceipt(mintTx) + + const issueTx = await borrowerTinlake.proxyTransferIssue(proxyAddr, testConfig.nftRegistry, tokenId) + const issueResult = await borrowerTinlake.getTransactionReceipt(issueTx) + + assert.equal(issueResult.status, FAIL_STATUS) }) + + // it('fail: does not succeed if the proxy is not approved to transfer currency from the borrower', async () => { + // // create new proxy and mint collateral NFT to borrower + // const proxyAddr = await borrowerTinlake.proxyCreateNew(borrowerAccount.address); + + // const nftId = await governanceTinlake.mintTitleNFT(testConfig.nftRegistry, borrowerAccount.address); + // const approveTx = await borrowerTinlake.approveNFT(testConfig.nftRegistry, nftId.toString(), proxyAddr); + // await borrowerTinlake.getTransactionReceipt(approveTx) + + // // issue loan from collateral NFT + // const proxyTransferTx = await borrowerTinlake.proxyTransferIssue(proxyAddr, testConfig.nftRegistry, nftId); + // await borrowerTinlake.getTransactionReceipt(proxyTransferTx) + + // // set loan parameters and fund tranche + // const loanId = await borrowerTinlake.nftLookup(testConfig.nftRegistry, nftId); + // const amount = 10; + // const relyTx = await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['NFT_FEED']); + // await governanceTinlake.getTransactionReceipt(relyTx) + + // const nftfeedId = await adminTinlake.getNftFeedId(testConfig.nftRegistry, Number(nftId)) + // const updateNftTx = await adminTinlake.updateNftFeed(nftfeedId, Number(amount)) + // await adminTinlake.getTransactionReceipt(updateNftTx) + + // await fundTranche('10000000'); + // await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR_TRANCHE']); + + // // borrow + // const proxyLockBorrowWithdrawTx = await borrowerTinlake.proxyLockBorrowWithdraw(proxyAddr, loanId, amount.toString(), borrowerAccount.address); + // await borrowerTinlake.getTransactionReceipt(proxyLockBorrowWithdrawTx) + + // await borrowerTinlake.getCurrencyBalance(borrowerAccount.address); + // await borrowerTinlake.getCurrencyBalance(contractAddresses['JUNIOR_TRANCHE']); + + // // does not approve proxy to transfer currency + // const mintTx = await governanceTinlake.mintCurrency(borrowerAccount.address, amount.toString()); + // await governanceTinlake.getTransactionReceipt(mintTx) + + // // repay + // const repayTx = await borrowerTinlake.proxyRepayUnlockClose(proxyAddr, nftId, loanId, testConfig.nftRegistry); + // const repayResult = await borrowerTinlake.getTransactionReceipt(repayTx) + // assert.equal(repayResult.status, FAIL_STATUS); + // }) }) }) // TODO: move to utils async function fundTranche(amount: string) { - const lenderAccount = account.generate(randomString.generate(32)) + const lenderAccount = ethers.Wallet.createRandom() const lenderTinlake = createTinlake(lenderAccount, testConfig) + // fund lender account with eth await testProvider.fundAccountWithETH(lenderAccount.address, FAUCET_AMOUNT) + // make admin address ward on tranche operator - await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['JUNIOR_OPERATOR']) + const fundTx = await governanceTinlake.relyAddress(adminAccount.address, contractAddresses['JUNIOR_OPERATOR']) + await governanceTinlake.getTransactionReceipt(fundTx) + // whitelist lender - await adminTinlake.approveAllowanceJunior(lenderAccount.address, amount, amount) + const approveAllowanceTx = await adminTinlake.approveAllowanceJunior(lenderAccount.address, amount, amount) + await adminTinlake.getTransactionReceipt(approveAllowanceTx) + // lender approves tranche to take currency - await lenderTinlake.approveCurrency(contractAddresses['JUNIOR'], amount) + const approveTx = await lenderTinlake.approveCurrency(contractAddresses['JUNIOR_TRANCHE'], amount) + await lenderTinlake.getTransactionReceipt(approveTx) + // mint currency for lender - await governanceTinlake.mintCurrency(lenderAccount.address, amount) + const mintTx = await governanceTinlake.mintCurrency(lenderAccount.address, amount) + governanceTinlake.getTransactionReceipt(mintTx) + // lender supplies tranche with funds - await lenderTinlake.supplyJunior(amount) + const supplyTx = await lenderTinlake.supplyJunior(amount) + await lenderTinlake.getTransactionReceipt(supplyTx) } diff --git a/src/actions/proxy.ts b/src/actions/proxy.ts index ca6809b..e595a3d 100644 --- a/src/actions/proxy.ts +++ b/src/actions/proxy.ts @@ -1,5 +1,4 @@ -import { Constructor, TinlakeParams } from '../Tinlake' -import { waitAndReturnEvents, executeAndRetry } from '../services/ethereum' +import { Constructor, TinlakeParams, PendingTransaction } from '../Tinlake' const abiCoder = require('web3-eth-abi') import BN from 'bn.js' import { ethers } from 'ethers' @@ -7,36 +6,37 @@ import { ethers } from 'ethers' export function ProxyActions>(Base: ActionsBase) { return class extends Base implements IProxyActions { getProxyAccessTokenOwner = async (tokenId: string): Promise => { - const res: { 0: BN } = await executeAndRetry(this.contracts['PROXY_REGISTRY'].ownerOf, [tokenId]) - return res[0] + return this.contract('PROXY_REGISTRY').ownerOf(tokenId) } buildProxy = async (owner: string) => { - const txHash = await executeAndRetry(this.contracts['PROXY_REGISTRY'].build, [owner, this.ethConfig]) - console.log(`[Proxy created] txHash: ${txHash}`) - const response: any = await waitAndReturnEvents( - this.eth, - txHash, - this.contracts['PROXY_REGISTRY'].abi, - this.transactionTimeout - ) - return response.events[0].data[2].toString() + const tx = await this.contract('PROXY_REGISTRY')['build(address)'](owner, this.overrides) + const receipt = await this.getTransactionReceipt(tx) + + if (!(receipt.logs && receipt.logs[1])) { + throw new Error('Created() event missing in proxyRegistry.build(address) receipt') + } + + // Two events are emitted: Transfer() (from the ERC721 contract mint method) and Created() (from the ProxyRegistry contract) + // We parse the 4th arg of the Created() event, to grab the access token + const parsedLog = this.contract('PROXY_REGISTRY').interface.parseLog(receipt.logs[1]) + const accessToken = parsedLog.values['3'].toNumber() + + return accessToken } getProxy = async (accessTokenId: string) => { - const res = await executeAndRetry(this.contracts['PROXY_REGISTRY'].proxies, [accessTokenId, this.ethConfig]) - return res[0] + return await this.contract('PROXY_REGISTRY').proxies(accessTokenId) } - getProxyAccessToken = async (proxyAddr: string) => { - const proxy: any = this.eth.contract(this.contractAbis['PROXY']).at(proxyAddr) - const res = await executeAndRetry(proxy.accessToken, []) - return res[0].toNumber() + getProxyAccessToken = async (proxyAddress: string) => { + const proxy = this.contract('PROXY', proxyAddress) + const accessToken = (await proxy.accessToken()).toBN() + return accessToken.toNumber() } getProxyOwnerByLoan = async (loanId: string) => { - const res = await executeAndRetry(this.contracts['TITLE'].ownerOf, [loanId]) - const loanOwner = res[0] + const loanOwner = this.contract('TITLE').ownerOf(loanId) const accessToken = await this.getProxyAccessToken(loanOwner) return this.getProxyAccessTokenOwner(accessToken) } @@ -47,8 +47,7 @@ export function ProxyActions>(Bas } proxyCount = async (): Promise => { - const res: { 0: BN } = await executeAndRetry(this.contracts['PROXY_REGISTRY'].count, []) - return res[0] + return (await this.contract('PROXY_REGISTRY').count()).toBN() } checkProxyExists = async (address: string): Promise => { @@ -65,116 +64,78 @@ export function ProxyActions>(Bas proxyCreateNew = async (address: string) => { const accessToken = await this.buildProxy(address) - return this.getProxy(accessToken) + return await this.getProxy(accessToken) } - proxyIssue = async (proxyAddr: string, nftRegistryAddr: string, tokenId: string) => { - const proxy: any = this.eth.contract(this.contractAbis['PROXY']).at(proxyAddr) - - const encoded = abiCoder.encodeFunctionCall( - { - name: 'issue', - type: 'function', - inputs: [ - { type: 'address', name: 'shelf' }, - { type: 'address', name: 'registry' }, - { type: 'uint256', name: 'token' }, - ], - }, - [this.contracts['SHELF'].address, nftRegistryAddr, tokenId] - ) - - const txHash = await executeAndRetry(proxy.execute, [this.contracts['ACTIONS'].address, encoded, this.ethConfig]) - console.log(`[Proxy Issue Loan] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contractAbis['PROXY'], this.transactionTimeout) + proxyIssue = async (proxyAddress: string, nftRegistryAddress: string, tokenId: string) => { + const proxy = this.contract('PROXY', proxyAddress) + const encoded = this.contract('ACTIONS').interface.functions.issue.encode([ + this.contract('SHELF').address, + nftRegistryAddress, + tokenId, + ]) + + return this.pending(proxy.execute(this.contract('ACTIONS').address, encoded, this.overrides)) } - proxyTransferIssue = async (proxyAddr: string, nftRegistryAddr: string, tokenId: string) => { - const proxy: any = this.eth.contract(this.contractAbis['PROXY']).at(proxyAddr) - - const encoded = abiCoder.encodeFunctionCall( - { - name: 'transferIssue', - type: 'function', - inputs: [ - { type: 'address', name: 'shelf' }, - { type: 'address', name: 'registry' }, - { type: 'uint256', name: 'token' }, - ], - }, - [this.contracts['SHELF'].address, nftRegistryAddr, tokenId] - ) - - const txHash = await executeAndRetry(proxy.execute, [this.contracts['ACTIONS'].address, encoded, this.ethConfig]) - console.log(`[Proxy Transfer Issue Loan] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contractAbis['PROXY'], this.transactionTimeout) + proxyTransferIssue = async (proxyAddress: string, nftRegistryAddress: string, tokenId: string) => { + const proxy = this.contract('PROXY', proxyAddress) + const encoded = this.contract('ACTIONS').interface.functions.transferIssue.encode([ + this.contract('SHELF').address, + nftRegistryAddress, + tokenId, + ]) + + return this.pending(proxy.execute(this.contract('ACTIONS').address, encoded, this.overrides)) } - proxyLockBorrowWithdraw = async (proxyAddr: string, loanId: string, amount: string, usr: string) => { - const proxy: any = this.eth.contract(this.contractAbis['PROXY']).at(proxyAddr) - const encoded = abiCoder.encodeFunctionCall( - { - name: 'lockBorrowWithdraw', - type: 'function', - inputs: [ - { type: 'address', name: 'shelf' }, - { type: 'uint256', name: 'loan' }, - { type: 'uint256', name: 'amount' }, - { type: 'address', name: 'usr' }, - ], - }, - [this.contracts['SHELF'].address, loanId, amount, usr] - ) - const txHash = await executeAndRetry(proxy.execute, [this.contracts['ACTIONS'].address, encoded, this.ethConfig]) - console.log(`[Proxy Lock Borrow Withdraw] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contractAbis['PROXY'], this.transactionTimeout) + proxyLockBorrowWithdraw = async (proxyAddress: string, loanId: string, amount: string, usr: string) => { + const proxy = this.contract('PROXY', proxyAddress) + const encoded = this.contract('ACTIONS').interface.functions.lockBorrowWithdraw.encode([ + this.contract('SHELF').address, + loanId, + amount, + usr, + ]) + + return this.pending(proxy.execute(this.contract('ACTIONS').address, encoded)) } - proxyRepayUnlockClose = async (proxyAddr: string, tokenId: string, loanId: string, registry: string) => { - const proxy: any = this.eth.contract(this.contractAbis['PROXY']).at(proxyAddr) - const encoded = abiCoder.encodeFunctionCall( - { - name: 'repayUnlockClose', - type: 'function', - inputs: [ - { type: 'address', name: 'shelf' }, - { type: 'address', name: 'pile' }, - { type: 'address', name: 'registry' }, - { type: 'uint256', name: 'token' }, - { type: 'address', name: 'erc20' }, - { type: 'uint256', name: 'loan' }, - ], - }, - [ - this.contracts['SHELF'].address, - this.contracts['PILE'].address, - registry, - tokenId, - this.contracts['TINLAKE_CURRENCY'].address, - loanId, - ] - ) - const txHash = await executeAndRetry(proxy.execute, [this.contracts['ACTIONS'].address, encoded, this.ethConfig]) - console.log(`[Proxy Repay Unlock Close] txHash: ${txHash}`) - return waitAndReturnEvents(this.eth, txHash, this.contractAbis['PROXY'], this.transactionTimeout) + proxyRepayUnlockClose = async (proxyAddress: string, tokenId: string, loanId: string, registry: string) => { + const proxy = this.contract('PROXY', proxyAddress) + const encoded = this.contract('ACTIONS').interface.functions.repayUnlockClose.encode([ + this.contract('SHELF').address, + this.contract('PILE').address, + registry, + tokenId, + this.contract('TINLAKE_CURRENCY').address, + loanId, + ]) + + return this.pending(proxy.execute(this.contract('ACTIONS').address, encoded, this.overrides)) } } } export type IProxyActions = { - buildProxy(owner: string): Promise + buildProxy(owner: string): Promise checkProxyExists(address: string): Promise - getProxy(accessTokenId: string): Promise - proxyCount(): Promise - getProxyAccessToken(proxyAddr: string): Promise - getProxyAccessTokenOwner(tokenId: string): Promise - getProxyOwnerByLoan(loanId: string): Promise - getProxyOwnerByAddress(proxyAddr: string): Promise - proxyCreateNew(address: string): Promise - proxyIssue(proxyAddr: string, nftRegistryAddr: string, tokenId: string): Promise - proxyTransferIssue(proxyAddr: string, nftRegistryAddr: string, tokenId: string): Promise - proxyLockBorrowWithdraw(proxyAddr: string, loanId: string, amount: string, usr: string): Promise - proxyRepayUnlockClose(proxyAddr: string, tokenId: string, loanId: string, registry: string): Promise + getProxy(accessTokenId: string): Promise + proxyCount(): Promise + getProxyAccessToken(proxyAddr: string): Promise + getProxyAccessTokenOwner(tokenId: string): Promise + getProxyOwnerByLoan(loanId: string): Promise + getProxyOwnerByAddress(proxyAddr: string): Promise + proxyCreateNew(address: string): Promise + proxyIssue(proxyAddr: string, nftRegistryAddr: string, tokenId: string): Promise + proxyTransferIssue(proxyAddr: string, nftRegistryAddr: string, tokenId: string): Promise + proxyLockBorrowWithdraw(proxyAddr: string, loanId: string, amount: string, usr: string): Promise + proxyRepayUnlockClose( + proxyAddr: string, + tokenId: string, + loanId: string, + registry: string + ): Promise } export default ProxyActions diff --git a/src/index.ts b/src/index.ts index 51bf707..558f1de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,9 @@ import actions from './actions/index' import Tinlake from './Tinlake' const { Admin, Borrower, Lender, Analytics, Currency, Collateral, Governance, Proxy, Coordinator } = actions -export const TinlakeWithActions = Coordinator(Proxy(Borrower(Admin(Lender(Analytics(Currency(Collateral(Governance(Tinlake))))))))) +export const TinlakeWithActions = Coordinator( + Proxy(Borrower(Admin(Lender(Analytics(Currency(Collateral(Governance(Tinlake)))))))) +) export default TinlakeWithActions export * from './types/tinlake' diff --git a/src/services/ethereum.ts b/src/services/ethereum.ts index 41727f5..57b3230 100644 --- a/src/services/ethereum.ts +++ b/src/services/ethereum.ts @@ -3,90 +3,12 @@ import { sha3 } from 'web3-utils' export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -export interface ethI { - send: Function - web3_sha3: (signature: string) => string - getTransactionReceipt: (arg0: any, arg1: (err: any, receipt: any) => void) => void - getTransactionByHash: (arg0: any, arg1: (err: any, tx: any) => void) => void - contract: (arg0: any) => { at: (arg0: any) => void } - sendRawTransaction: any - getTransactionCount: any - abi: any -} - export interface Events { txHash: string status: any events: { event: { name: any }; data: any[] }[] } -export async function executeAndRetry(f: Function, args: any = []): Promise { - try { - const result = await f(...args) - return result - } catch (e) { - // using error message, since error code -32603 is not unique enough - // todo introduce retry limit - if ( - e && - e.message && - (e.message.indexOf("Cannot read property 'number' of null") !== -1 || - e.message.indexOf('error with payload') !== -1) - ) { - console.log('internal RPC error detected, retry triggered...', e) - throw new Error('Internal RPC Error. Please try again.') - // await sleep(1000); - // return executeAndRetry(f, args); - } else { - throw e - } - } -} - -export const waitAndReturnEvents = async (eth: ethI, txHash: string, abi: any, transactionTimeout: number) => { - const tx: any = await waitForTransaction(eth, txHash, transactionTimeout) - return new Promise((resolve, reject) => { - eth.getTransactionReceipt(tx.hash, (err: null, receipt: any) => { - if (err != null) { - reject('failed to get receipt') - } - const events = getEvents(receipt, abi) - resolve({ events, txHash: tx.hash, status: receipt.status }) - }) - }) -} - -// todo replace with a better polling -// TODO : use polling interval from config -export const waitForTransaction = (eth: ethI, txHash: any, transactionTimeout: number) => { - return new Promise((resolve, reject) => { - const secMax = transactionTimeout - let sec = 0 - const wait = (txHash: string) => { - setTimeout(() => { - eth.getTransactionByHash(txHash, (err: any, tx: any) => { - if (err) { - reject(err) - return - } - if (tx && tx.blockHash != null) { - resolve(tx) - return - } - console.log(`waiting for tx :${txHash}`) - sec = sec + 1 - if (sec < secMax) { - wait(txHash) - } else { - reject(new Error(`waiting for transaction tx ${txHash} timed out after ${secMax} seconds`)) - } - }) - }, 1000) - } - wait(txHash) - }) -} - export const findEvent = (abi: { filter: (arg0: (item: any) => boolean | undefined) => any[] }, funcSignature: any) => { return abi.filter( (item: { diff --git a/src/test/addresses.json b/src/test/addresses.json index ecd8d46..37ce6a8 100644 --- a/src/test/addresses.json +++ b/src/test/addresses.json @@ -1,9 +1,9 @@ { "TINLAKE_CURRENCY":"", "JUNIOR_OPERATOR":"", - "JUNIOR":"", + "JUNIOR_TRANCHE":"", "JUNIOR_TOKEN":"", - "SENIOR":"", + "SENIOR_TRANCHE":"", "SENIOR_TOKEN":"", "SENIOR_OPERATOR":"", "DISTRIBUTOR":"", diff --git a/src/test/config.ts b/src/test/config.ts index d5b9708..ad61953 100644 --- a/src/test/config.ts +++ b/src/test/config.ts @@ -1,36 +1,29 @@ import contractAddresses from './addresses.json' import abiDefinitions from '../abi/' -import { Account } from './types' import { ContractAddresses, ContractAbis } from '../Tinlake' import dotenv from 'dotenv' +import { ethers } from 'ethers' dotenv.config() -const KWEI = 1000 -const MWEI = 1000 * KWEI -const GWEI = 1000 * MWEI const MILLI_ETH = 1e15 // 0.001 ETH const FAUCET_AMOUNT = 5000 * MILLI_ETH -const GAS_PRICE = 5 * GWEI -const GAS = 1000000 +const GAS_LIMIT = 1000000 const testConfig: ProviderConfig = { contractAddresses: (process.env.CONTRACTS && JSON.parse(process.env.CONTRACTS)) || contractAddresses, - godAccount: { - address: process.env.GOD_ADDRESS || '0xf6fa8a3f3199cdd85749ec749fb8f9c2551f9928', - publicKey: process.env.GOD_PUB_KEY || '', - privateKey: process.env.GOD_PRIV_KEY || '0xb2e0c8e791c37df214808cdadc187f0cba0e36160f1a38b321a25c9a0cea8c11', - }, + godAccount: new ethers.Wallet( + process.env.GOD_PRIV_KEY || '0xb2e0c8e791c37df214808cdadc187f0cba0e36160f1a38b321a25c9a0cea8c11' + ), nftRegistry: process.env.NFT_REGISTRY || '0xac0c1ef395290288028a0a9fdfc8fdebebe54a24', transactionTimeout: 50000, - gasPrice: `${GAS_PRICE}`, - gas: `${GAS}`, + overrides: { gasLimit: GAS_LIMIT }, rpcUrl: process.env.RPC_URL || 'http://127.0.0.1:8545', isRealTestnet: false, contractAbis: abiDefinitions, - SUCCESS_STATUS: '0x1', - FAIL_STATUS: '0x0', + SUCCESS_STATUS: 1, + FAIL_STATUS: 0, FAUCET_AMOUNT: `${FAUCET_AMOUNT}`, } @@ -39,16 +32,15 @@ testConfig.isRealTestnet = !testConfig.rpcUrl.includes('127.0.0.1') && !testConf export type ProviderConfig = { rpcUrl: string isRealTestnet: boolean - godAccount: Account - gas: string - gasPrice: string + godAccount: ethers.Wallet nftRegistry: string transactionTimeout: number contractAddresses: ContractAddresses contractAbis: ContractAbis - SUCCESS_STATUS: '0x1' - FAIL_STATUS: '0x0' + SUCCESS_STATUS: 1 + FAIL_STATUS: 0 FAUCET_AMOUNT: string + overrides: ethers.providers.TransactionRequest } export default testConfig diff --git a/src/test/utils.ts b/src/test/utils.ts index ef30aaf..f3a7f1c 100644 --- a/src/test/utils.ts +++ b/src/test/utils.ts @@ -1,22 +1,17 @@ -import { Account } from './types' import Tinlake from '..' -import { EthConfig } from '../Tinlake' import { ITinlake } from '../types/tinlake' import { ProviderConfig } from './config' -import { ethers, Signer } from 'ethers' -const SignerProvider = require('ethjs-provider-signer') -const { sign } = require('ethjs-signer') +import { ethers, Wallet } from 'ethers' export class TestProvider { + public provider: ethers.providers.Provider public wallet: ethers.Wallet - public ethConfig: EthConfig public transactionTimeout: number constructor(testConfig: ProviderConfig) { const { rpcUrl, godAccount, transactionTimeout } = testConfig - const provider = new ethers.providers.JsonRpcProvider(rpcUrl) - this.wallet = new ethers.Wallet(godAccount.privateKey, provider) - this.ethConfig = { from: godAccount.address } + this.provider = new ethers.providers.JsonRpcProvider(rpcUrl) + this.wallet = new ethers.Wallet(godAccount.privateKey, this.provider) this.transactionTimeout = transactionTimeout } @@ -27,26 +22,21 @@ export class TestProvider { } const res = await this.wallet.sendTransaction(transaction) - await res.wait(1) + await this.provider.waitForTransaction(res.hash!) } } -export function createTinlake(usr: Account, testConfig: ProviderConfig): ITinlake { - const { rpcUrl, transactionTimeout, gas, gasPrice, contractAddresses } = testConfig +export function createTinlake(wallet: Wallet, testConfig: ProviderConfig): ITinlake { + const { rpcUrl, transactionTimeout, contractAddresses } = testConfig + const provider = new ethers.providers.JsonRpcProvider(rpcUrl) const tinlake = new Tinlake({ contractAddresses, transactionTimeout, - provider: createSignerProvider(rpcUrl, usr), - ethConfig: { gas, gasPrice, from: usr.address }, + provider, + signer: wallet.connect(provider), + overrides: testConfig.overrides, }) return tinlake } - -function createSignerProvider(rpcUrl: string, usr: Account) { - return new SignerProvider(rpcUrl, { - signTransaction: (rawTx: any, cb: (arg0: null, arg1: any) => void) => cb(null, sign(rawTx, usr.privateKey)), - accounts: (cb: (arg0: null, arg1: string[]) => void) => cb(null, [usr.address]), - }) -} diff --git a/src/types/declarations.d.ts b/src/types/declarations.d.ts index 98da937..b7b3580 100644 --- a/src/types/declarations.d.ts +++ b/src/types/declarations.d.ts @@ -6,4 +6,4 @@ declare module '*.abi' { declare module 'ethjs' declare module 'web3-utils' -declare module 'glpk.js' \ No newline at end of file +declare module 'glpk.js' diff --git a/src/types/tinlake.ts b/src/types/tinlake.ts index 5fe52ce..ae748fe 100644 --- a/src/types/tinlake.ts +++ b/src/types/tinlake.ts @@ -1,6 +1,6 @@ import { TinlakeActions } from '../actions' import BN from 'bn.js' -import Tinlake, { EthConfig, ContractAddresses, ContractAbis, Contracts } from '../Tinlake' +import Tinlake, { PendingTransaction, ContractAddresses, ContractAbis, Contracts } from '../Tinlake' export type Loan = { loanId: string @@ -46,3 +46,4 @@ export type Investor = { } export type ITinlake = TinlakeActions & Tinlake +export { PendingTransaction, ContractAddresses, ContractAbis, Contracts }