From 9e3da0804457cc30776d3499b896ba958221cebd Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 11:25:39 -0700 Subject: [PATCH 01/36] make --- package-lock.json | 176 +++++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0358b1ff49..1adf9bf1da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "indra", - "version": "7.0.0-alpha.16", + "version": "7.0.0-alpha.18", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -15247,12 +15247,12 @@ "dependencies": { "ansi-regex": { "version": "4.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "ansi-styles": { "version": "3.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { "color-convert": "^1.9.0" @@ -15260,7 +15260,7 @@ }, "bindings": { "version": "1.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "requires": { "file-uri-to-path": "1.0.0" @@ -15268,7 +15268,7 @@ }, "bip66": { "version": "1.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", "requires": { "safe-buffer": "^5.0.1" @@ -15276,17 +15276,17 @@ }, "bn.js": { "version": "4.11.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "brorand": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browserify-aes": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "requires": { "buffer-xor": "^1.0.3", @@ -15299,22 +15299,22 @@ }, "buffer-from": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-xor": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "camelcase": { "version": "5.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "cipher-base": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "requires": { "inherits": "^2.0.1", @@ -15323,7 +15323,7 @@ }, "cliui": { "version": "5.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "requires": { "string-width": "^3.1.0", @@ -15333,7 +15333,7 @@ }, "color-convert": { "version": "1.9.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "requires": { "color-name": "1.1.3" @@ -15341,12 +15341,12 @@ }, "color-name": { "version": "1.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "create-hash": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { "cipher-base": "^1.0.1", @@ -15358,7 +15358,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { "cipher-base": "^1.0.3", @@ -15371,7 +15371,7 @@ }, "cross-spawn": { "version": "6.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "requires": { "nice-try": "^1.0.4", @@ -15383,12 +15383,12 @@ }, "decamelize": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "drbg.js": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", "requires": { "browserify-aes": "^1.0.6", @@ -15398,7 +15398,7 @@ }, "elliptic": { "version": "6.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", "requires": { "bn.js": "^4.4.0", @@ -15412,12 +15412,12 @@ }, "emoji-regex": { "version": "7.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "end-of-stream": { "version": "1.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "requires": { "once": "^1.4.0" @@ -15425,7 +15425,7 @@ }, "ethereumjs-util": { "version": "6.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", "requires": { "bn.js": "^4.11.0", @@ -15439,7 +15439,7 @@ }, "ethjs-util": { "version": "0.1.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", "requires": { "is-hex-prefixed": "1.0.0", @@ -15448,7 +15448,7 @@ }, "evp_bytestokey": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "requires": { "md5.js": "^1.3.4", @@ -15457,7 +15457,7 @@ }, "execa": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "requires": { "cross-spawn": "^6.0.0", @@ -15471,12 +15471,12 @@ }, "file-uri-to-path": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "find-up": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { "locate-path": "^3.0.0" @@ -15484,12 +15484,12 @@ }, "get-caller-file": { "version": "2.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-stream": { "version": "4.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "requires": { "pump": "^3.0.0" @@ -15497,7 +15497,7 @@ }, "hash-base": { "version": "3.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "requires": { "inherits": "^2.0.1", @@ -15506,7 +15506,7 @@ }, "hash.js": { "version": "1.1.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "requires": { "inherits": "^2.0.3", @@ -15515,7 +15515,7 @@ }, "hmac-drbg": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "requires": { "hash.js": "^1.0.3", @@ -15525,37 +15525,37 @@ }, "inherits": { "version": "2.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "invert-kv": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" }, "is-fullwidth-code-point": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "is-hex-prefixed": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=" }, "is-stream": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "isexe": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "keccak": { "version": "1.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", "requires": { "bindings": "^1.2.1", @@ -15566,7 +15566,7 @@ }, "lcid": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "requires": { "invert-kv": "^2.0.0" @@ -15574,7 +15574,7 @@ }, "locate-path": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { "p-locate": "^3.0.0", @@ -15583,7 +15583,7 @@ }, "map-age-cleaner": { "version": "0.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", "requires": { "p-defer": "^1.0.0" @@ -15591,7 +15591,7 @@ }, "md5.js": { "version": "1.3.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "requires": { "hash-base": "^3.0.0", @@ -15601,7 +15601,7 @@ }, "mem": { "version": "4.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "requires": { "map-age-cleaner": "^0.1.1", @@ -15611,32 +15611,32 @@ }, "mimic-fn": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "minimalistic-assert": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "nan": { "version": "2.14.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "nice-try": { "version": "1.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "npm-run-path": { "version": "2.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "requires": { "path-key": "^2.0.0" @@ -15644,7 +15644,7 @@ }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" @@ -15652,7 +15652,7 @@ }, "os-locale": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "requires": { "execa": "^1.0.0", @@ -15662,22 +15662,22 @@ }, "p-defer": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, "p-finally": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-is-promise": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" }, "p-limit": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", "requires": { "p-try": "^2.0.0" @@ -15685,7 +15685,7 @@ }, "p-locate": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { "p-limit": "^2.0.0" @@ -15693,22 +15693,22 @@ }, "p-try": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "path-exists": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-key": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "pump": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "requires": { "end-of-stream": "^1.1.0", @@ -15717,17 +15717,17 @@ }, "require-directory": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "ripemd160": { "version": "2.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "requires": { "hash-base": "^3.0.0", @@ -15736,7 +15736,7 @@ }, "rlp": { "version": "2.2.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.3.tgz", "integrity": "sha512-l6YVrI7+d2vpW6D6rS05x2Xrmq8oW7v3pieZOJKBEdjuTF4Kz/iwk55Zyh1Zaz+KOB2kC8+2jZlp2u9L4tTzCQ==", "requires": { "bn.js": "^4.11.1", @@ -15745,12 +15745,12 @@ }, "safe-buffer": { "version": "5.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" }, "secp256k1": { "version": "3.7.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.7.1.tgz", "integrity": "sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==", "requires": { "bindings": "^1.5.0", @@ -15765,17 +15765,17 @@ }, "semver": { "version": "5.7.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "sha.js": { "version": "2.4.11", - "resolved": false, + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { "inherits": "^2.0.1", @@ -15784,7 +15784,7 @@ }, "shebang-command": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "requires": { "shebang-regex": "^1.0.0" @@ -15792,22 +15792,22 @@ }, "shebang-regex": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "source-map": { "version": "0.6.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-support": { "version": "0.5.12", - "resolved": false, + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", "requires": { "buffer-from": "^1.0.0", @@ -15816,7 +15816,7 @@ }, "string-width": { "version": "3.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { "emoji-regex": "^7.0.1", @@ -15826,7 +15826,7 @@ }, "strip-ansi": { "version": "5.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { "ansi-regex": "^4.1.0" @@ -15834,12 +15834,12 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-hex-prefix": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", "integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=", "requires": { "is-hex-prefixed": "1.0.0" @@ -15847,7 +15847,7 @@ }, "which": { "version": "1.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "requires": { "isexe": "^2.0.0" @@ -15855,12 +15855,12 @@ }, "which-module": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wrap-ansi": { "version": "5.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "requires": { "ansi-styles": "^3.2.0", @@ -15870,17 +15870,17 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "y18n": { "version": "4.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yargs": { "version": "13.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", "requires": { "cliui": "^5.0.0", @@ -15898,7 +15898,7 @@ }, "yargs-parser": { "version": "13.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "requires": { "camelcase": "^5.0.0", From a16966f56a0be7ba26393129eeb9791914e628b6 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 11:25:45 -0700 Subject: [PATCH 02/36] event bugfix --- modules/client/src/listener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/client/src/listener.ts b/modules/client/src/listener.ts index 5b9a4c1b8c..18432e7071 100644 --- a/modules/client/src/listener.ts +++ b/modules/client/src/listener.ts @@ -409,7 +409,7 @@ export class ConnextListener { timelock: meta["timelock"], } as CreatedHashLockTransferMeta, type: ConditionalTransferTypes.HashLockTransfer, - paymentId: initialState.lockHash, + paymentId: meta["paymentId"], recipient: meta["recipient"], } as EventPayloads.HashLockTransferCreated); break; From cd4a5f1a95f53a3bf86e1d64fb1c03fe22fb1896 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 11:25:51 -0700 Subject: [PATCH 03/36] heavy test refactor --- .../src/transfer/hashLockTransfer.test.ts | 588 +++++++----------- 1 file changed, 212 insertions(+), 376 deletions(-) diff --git a/modules/test-runner/src/transfer/hashLockTransfer.test.ts b/modules/test-runner/src/transfer/hashLockTransfer.test.ts index f160605d20..23919bf52d 100644 --- a/modules/test-runner/src/transfer/hashLockTransfer.test.ts +++ b/modules/test-runner/src/transfer/hashLockTransfer.test.ts @@ -6,8 +6,7 @@ import { IConnextClient, NodeResponses, PublicParams, - EventPayloads, - UnlockedHashLockTransferMeta, + ConditionalTransferCreatedEventData, } from "@connext/types"; import { getRandomBytes32, getChainId } from "@connext/utils"; import { BigNumber, providers, constants, utils } from "ethers"; @@ -26,7 +25,9 @@ import { const { AddressZero, HashZero } = constants; const { soliditySha256 } = utils; -describe("HashLock Transfers", () => { +const TIMEOUT_BUFFER = 100; // This currently isn't exported by the node so must be hardcoded + +describe.only("HashLock Transfers", () => { let clientA: IConnextClient; let clientB: IConnextClient; let tokenAddress: string; @@ -38,9 +39,6 @@ describe("HashLock Transfers", () => { await getChainId(env.ethProviderUrl), ); const currBlock = await provider.getBlockNumber(); - // the node uses a `TIMEOUT_BUFFER` on recipient of 100 blocks - // so make sure the current block - const TIMEOUT_BUFFER = 100; if (currBlock > TIMEOUT_BUFFER) { // no adjustment needed, return return; @@ -50,6 +48,148 @@ describe("HashLock Transfers", () => { } }); + // Define helper functions + const sendHashlockTransfer = async ( + sender: IConnextClient, + receiver: IConnextClient, + transfer: AssetOptions & { preImage: string; timelock: string }, + ): Promise> => { + // Fund sender channel + await fundChannel(sender, transfer.amount, transfer.assetId); + + // Create transfer parameters + const expiry = BigNumber.from(transfer.timelock).add(await provider.getBlockNumber()); + const lockHash = soliditySha256(["bytes32"], [transfer.preImage]); + + // return promise with [sender ret, receiver event data] + const [senderResult, receiverEvent] = await Promise.all([ + // sender result + clientA.conditionalTransfer({ + amount: transfer.amount.toString(), + conditionType: ConditionalTransferTypes.HashLockTransfer, + lockHash, + timelock: transfer.timelock, + assetId: transfer.assetId, + meta: { foo: "bar", sender: clientA.publicIdentifier }, + recipient: clientB.publicIdentifier, + }), + // receiver created event + new Promise((resolve) => { + receiver.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, (eventPayload) => + resolve(eventPayload), + ); + }), + ]); + const paymentId = soliditySha256(["address", "bytes32"], [transfer.assetId, lockHash]); + const expectedVals = { + amount: transfer.amount, + assetId: transfer.assetId, + paymentId, + recipient: receiver.publicIdentifier, + sender: sender.publicIdentifier, + transferMeta: { + timelock: transfer.timelock, + lockHash, + expiry: expiry.sub(TIMEOUT_BUFFER), + }, + }; + // verify the receiver event + expect(receiverEvent).to.containSubset({ + ...expectedVals, + type: ConditionalTransferTypes.HashLockTransfer, + }); + + // verify sender return value + expect(senderResult).to.containSubset({ + ...expectedVals, + transferMeta: { + ...expectedVals.transferMeta, + expiry, + }, + }); + return receiverEvent as any; + }; + + // returns [resolveResult, undefined, undefined] + const waitForResolve = ( + sender: IConnextClient, + receiver: IConnextClient, + transfer: AssetOptions & { preImage: string; timelock: string }, + waitForSender: boolean = true, + ) => { + return Promise.all([ + // receiver result + receiver.resolveCondition({ + conditionType: ConditionalTransferTypes.HashLockTransfer, + preImage: transfer.preImage, + assetId: transfer.assetId, + }), + // receiver event + new Promise((resolve, reject) => { + receiver.once(EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, resolve); + receiver.once(EventNames.CONDITIONAL_TRANSFER_FAILED_EVENT, reject); + }), + // sender event + new Promise((resolve, reject) => { + if (waitForSender) { + sender.once(EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, resolve); + sender.once(EventNames.CONDITIONAL_TRANSFER_FAILED_EVENT, reject); + } else { + resolve(); + } + }), + ]); + }; + + const assertSenderDecrement = async ( + sender: IConnextClient, + transfer: AssetOptions & { preImage: string; timelock: string }, + ) => { + const { [sender.signerAddress]: senderBal } = await sender.getFreeBalance(transfer.assetId); + expect(senderBal).to.eq(0); + }; + + const assertPostTransferBalances = async ( + sender: IConnextClient, + receiver: IConnextClient, + transfer: AssetOptions & { preImage: string; timelock: string }, + ) => { + const { [sender.signerAddress]: senderBal } = await sender.getFreeBalance(transfer.assetId); + expect(senderBal).to.eq(0); + const { [receiver.signerAddress]: receiverBal } = await receiver.getFreeBalance( + transfer.assetId, + ); + expect(receiverBal).to.eq(transfer.amount); + }; + + const assertRetrievedTransfer = async ( + client: IConnextClient, + transfer: AssetOptions & { + timelock: string; + preImage: string; + senderIdentifier: string; + receiverIdentifier: string; + paymentId: string; + }, + expected: Partial = {}, + ) => { + const lockHash = soliditySha256(["bytes32"], [transfer.preImage]); + const retrieved = await client.getHashLockTransfer(lockHash, transfer.assetId); + expect(retrieved).to.containSubset({ + amount: transfer.amount.toString(), + assetId: transfer.assetId, + lockHash, + senderIdentifier: transfer.senderIdentifier, + receiverIdentifier: transfer.receiverIdentifier, + meta: { + sender: transfer.senderIdentifier, + timelock: transfer.timelock, + paymentId: transfer.paymentId, + }, + ...expected, + }); + }; + beforeEach(async () => { clientA = await createClient({ id: "A" }); clientB = await createClient({ id: "B" }); @@ -63,329 +203,110 @@ describe("HashLock Transfers", () => { it("happy case: client A hashlock transfers eth to client B through node", async () => { const transfer: AssetOptions = { amount: ETH_AMOUNT_SM, assetId: AddressZero }; - await fundChannel(clientA, transfer.amount, transfer.assetId); const preImage = getRandomBytes32(); const timelock = (5000).toString(); - const expiry = BigNumber.from(timelock).add(await provider.getBlockNumber()); + const opts = { ...transfer, preImage, timelock }; - const lockHash = soliditySha256(["bytes32"], [preImage]); + await sendHashlockTransfer(clientA, clientB, opts); - await Promise.all([ - new Promise((res) => { - clientB.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, (eventPayload) => { - expect(eventPayload).to.deep.contain({ - amount: transfer.amount, - assetId: transfer.assetId, - type: ConditionalTransferTypes.HashLockTransfer, - paymentId: lockHash, - recipient: clientB.publicIdentifier, - } as EventPayloads.HashLockTransferCreated); - expect(eventPayload.transferMeta).to.deep.eq({ - timelock, - lockHash, - expiry: expiry.sub(100), - }); - return res(); - }); - }), - // new Promise((reso) => { - // clientA.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, (eventPayload) => { - // console.log("clientAeventPayload: ", eventPayload); - // expect(eventPayload).to.deep.contain({ - // amount: transfer.amount, - // assetId: transfer.assetId, - // type: ConditionalTransferTypes.HashLockTransfer, - // paymentId: lockHash, - // recipient: clientB.publicIdentifier, - // } as EventPayloads.HashLockTransferCreated); - // expect(eventPayload.transferMeta).to.deep.eq({ - // timelock, - // lockHash, - // expiry, - // }); - // console.log("RESOLVING A"); - // return reso(); - // }); - // }), - clientA.conditionalTransfer({ - amount: transfer.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transfer.assetId, - meta: { foo: "bar", sender: clientA.publicIdentifier }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer), - ]); + await assertSenderDecrement(clientA, opts); - const { - [clientA.signerAddress]: clientAPostTransferBal, - [clientA.nodeSignerAddress]: nodePostTransferBal, - } = await clientA.getFreeBalance(transfer.assetId); - expect(clientAPostTransferBal).to.eq(0); + await waitForResolve(clientA, clientB, opts); - await Promise.all([ - new Promise(async (res) => { - clientA.once(EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, res); - }), - new Promise(async (res) => { - clientB.once(EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, res); - }), - clientB.resolveCondition({ - conditionType: ConditionalTransferTypes.HashLockTransfer, - preImage, - assetId: transfer.assetId, - } as PublicParams.ResolveHashLockTransfer), - ]); - const { - [clientA.signerAddress]: clientAPostReclaimBal, - [clientA.nodeSignerAddress]: nodePostReclaimBal, - } = await clientA.getFreeBalance(transfer.assetId); - expect(clientAPostReclaimBal).to.eq(0); - expect(nodePostReclaimBal).to.eq(nodePostTransferBal.add(transfer.amount)); - const { [clientB.signerAddress]: clientBPostTransferBal } = await clientB.getFreeBalance( - transfer.assetId, - ); - expect(clientBPostTransferBal).to.eq(transfer.amount); + await assertPostTransferBalances(clientA, clientB, opts); }); it("happy case: client A hashlock transfers tokens to client B through node", async () => { const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; - await fundChannel(clientA, transfer.amount, transfer.assetId); const preImage = getRandomBytes32(); const timelock = (5000).toString(); - const expiry = BigNumber.from(timelock).add(await provider.getBlockNumber()); + const opts = { ...transfer, preImage, timelock }; - const lockHash = soliditySha256(["bytes32"], [preImage]); - // both sender + receiver apps installed, sender took action - await Promise.all([ - clientA.conditionalTransfer({ - amount: transfer.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transfer.assetId, - meta: { foo: "bar", sender: clientA.publicIdentifier }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer), - new Promise((res) => { - clientB.on(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, (eventPayload) => { - expect(eventPayload).to.deep.contain({ - amount: transfer.amount, - assetId: transfer.assetId, - type: ConditionalTransferTypes.HashLockTransfer, - paymentId: lockHash, - recipient: clientB.publicIdentifier, - } as EventPayloads.HashLockTransferCreated); - expect(eventPayload.transferMeta).to.deep.eq({ - timelock, - lockHash, - expiry: expiry.sub(100), - }); - res(); - }); - }), - ]); + await sendHashlockTransfer(clientA, clientB, opts); - const { - [clientA.signerAddress]: clientAPostTransferBal, - [clientA.nodeSignerAddress]: nodePostTransferBal, - } = await clientA.getFreeBalance(transfer.assetId); - expect(clientAPostTransferBal).to.eq(0); - - await new Promise(async (res) => { - clientA.on(EventNames.UNINSTALL_EVENT, async (data) => { - const { - [clientA.signerAddress]: clientAPostReclaimBal, - [clientA.nodeSignerAddress]: nodePostReclaimBal, - } = await clientA.getFreeBalance(transfer.assetId); - expect(clientAPostReclaimBal).to.eq(0); - expect(nodePostReclaimBal).to.eq(nodePostTransferBal.add(transfer.amount)); - res(); - }); - await clientB.resolveCondition({ - conditionType: ConditionalTransferTypes.HashLockTransfer, - preImage, - assetId: transfer.assetId, - } as PublicParams.ResolveHashLockTransfer); - const { [clientB.signerAddress]: clientBPostTransferBal } = await clientB.getFreeBalance( - transfer.assetId, - ); - expect(clientBPostTransferBal).to.eq(transfer.amount); - }); + await assertSenderDecrement(clientA, opts); + + await waitForResolve(clientA, clientB, opts); + + await assertPostTransferBalances(clientA, clientB, opts); }); it("gets a pending hashlock transfer by lock hash", async () => { - const TIMEOUT_BUFFER = 100; // This currently isn't exported by the node so must be hardcoded const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; - await fundChannel(clientA, transfer.amount, transfer.assetId); const preImage = getRandomBytes32(); const timelock = (5000).toString(); - - const lockHash = soliditySha256(["bytes32"], [preImage]); - const paymentId = soliditySha256(["address", "bytes32"], [transfer.assetId, lockHash]); - const expiry = BigNumber.from(await provider.getBlockNumber()) - .add(timelock) - .sub(TIMEOUT_BUFFER); - // both sender + receiver apps installed, sender took action - clientA.conditionalTransfer({ - amount: transfer.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transfer.assetId, - meta: { foo: "bar", sender: clientA.publicIdentifier }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer); - await new Promise((res) => clientB.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, res)); - - const retrievedTransfer = await clientB.getHashLockTransfer(lockHash, transfer.assetId); - expect(retrievedTransfer).to.deep.equal({ - amount: transfer.amount.toString(), - assetId: transfer.assetId, - lockHash, - senderIdentifier: clientA.publicIdentifier, - receiverIdentifier: clientB.publicIdentifier, - status: HashLockTransferStatus.PENDING, - meta: { foo: "bar", sender: clientA.publicIdentifier, timelock, paymentId }, - preImage: HashZero, - expiry, - } as NodeResponses.GetHashLockTransfer); + const opts = { ...transfer, preImage, timelock }; + + const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); + + await assertRetrievedTransfer( + clientB, + { + ...opts, + senderIdentifier: clientA.publicIdentifier, + receiverIdentifier: clientB.publicIdentifier, + paymentId: paymentId!, + }, + { + status: HashLockTransferStatus.PENDING, + preImage: HashZero, + }, + ); }); it("gets a completed hashlock transfer by lock hash", async () => { - const TIMEOUT_BUFFER = 100; // This currently isn't exported by the node so must be hardcoded const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; - await fundChannel(clientA, transfer.amount, transfer.assetId); const preImage = getRandomBytes32(); const timelock = (5000).toString(); - const expiry = BigNumber.from(await provider.getBlockNumber()) - .add(timelock) - .sub(TIMEOUT_BUFFER); - - const lockHash = soliditySha256(["bytes32"], [preImage]); - const paymentId = soliditySha256(["address", "bytes32"], [transfer.assetId, lockHash]); - - // both sender + receiver apps installed, sender took action - await new Promise((resolve, reject) => { - clientB.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, resolve); - clientA - .conditionalTransfer({ - amount: transfer.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transfer.assetId, - meta: { foo: "bar", sender: clientA.publicIdentifier }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer) - .catch((e) => reject(e.message)); - }); - - // wait for transfer to be picked up by receiver - await new Promise(async (resolve, reject) => { - // Note: MUST wait for uninstall, bc UNLOCKED gets thrown on takeAction - // at the moment, there's no way to filter the uninstalled app here so - // we're just gonna resolve and hope for the best - clientB.once( - EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, - resolve, - (data) => (data.transferMeta as UnlockedHashLockTransferMeta).preImage === preImage, - ); - clientB.once(EventNames.CONDITIONAL_TRANSFER_FAILED_EVENT, reject); - await clientB.resolveCondition({ - conditionType: ConditionalTransferTypes.HashLockTransfer, + const opts = { ...transfer, preImage, timelock }; + + const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); + + // wait for transfer to be picked up by receiver, but not reclaim + // by node for sender + await waitForResolve(clientA, clientB, opts, false); + + await assertRetrievedTransfer( + clientB, + { + ...opts, + senderIdentifier: clientA.publicIdentifier, + receiverIdentifier: clientB.publicIdentifier, + paymentId: paymentId!, + }, + { + status: HashLockTransferStatus.COMPLETED, preImage, - assetId: transfer.assetId, - }); - }); - - const retrievedTransfer = await clientB.getHashLockTransfer(lockHash, transfer.assetId); - expect(retrievedTransfer).to.deep.equal({ - amount: transfer.amount.toString(), - assetId: transfer.assetId, - lockHash, - senderIdentifier: clientA.publicIdentifier, - receiverIdentifier: clientB.publicIdentifier, - status: HashLockTransferStatus.COMPLETED, - preImage, - expiry, - meta: { foo: "bar", sender: clientA.publicIdentifier, timelock, paymentId }, - } as NodeResponses.GetHashLockTransfer); + }, + ); }); it("can send two hashlock transfers with different assetIds and the same lock hash", async () => { const transferToken: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; - await fundChannel(clientA, transferToken.amount, transferToken.assetId); const transferEth: AssetOptions = { amount: ETH_AMOUNT_SM, assetId: AddressZero }; - await fundChannel(clientA, transferEth.amount, transferEth.assetId); const preImage = getRandomBytes32(); const timelock = (5000).toString(); - const lockHash = soliditySha256(["bytes32"], [preImage]); - clientA.conditionalTransfer({ - amount: transferToken.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transferToken.assetId, - meta: { foo: "bar" }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer); - await new Promise((res) => clientB.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, res)); - - clientA.conditionalTransfer({ - amount: transferEth.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transferEth.assetId, - meta: { foo: "bar" }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer); - await new Promise((res) => clientB.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, res)); - - await clientB.resolveCondition({ - conditionType: ConditionalTransferTypes.HashLockTransfer, - preImage, - assetId: transferToken.assetId, - } as PublicParams.ResolveHashLockTransfer); - - await clientB.resolveCondition({ - conditionType: ConditionalTransferTypes.HashLockTransfer, - preImage, - assetId: transferEth.assetId, - } as PublicParams.ResolveHashLockTransfer); - - const { [clientB.signerAddress]: freeBalanceToken } = await clientB.getFreeBalance( - transferToken.assetId, - ); - const { [clientB.signerAddress]: freeBalanceEth } = await clientB.getFreeBalance( - transferEth.assetId, - ); + const ethOpts = { ...transferEth, preImage, timelock }; + const tokenOpts = { ...transferToken, preImage, timelock }; + + await sendHashlockTransfer(clientA, clientB, ethOpts); + await sendHashlockTransfer(clientA, clientB, tokenOpts); + + await waitForResolve(clientA, clientB, ethOpts); + await waitForResolve(clientA, clientB, tokenOpts); - expect(freeBalanceToken).to.eq(transferToken.amount); - expect(freeBalanceEth).to.eq(transferEth.amount); + await assertPostTransferBalances(clientA, clientB, ethOpts); + await assertPostTransferBalances(clientA, clientB, tokenOpts); }); it("cannot resolve a hashlock transfer if pre image is wrong", async () => { const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; - await fundChannel(clientA, transfer.amount, transfer.assetId); const preImage = getRandomBytes32(); const timelock = (5000).toString(); + const opts = { ...transfer, preImage, timelock }; - const lockHash = soliditySha256(["bytes32"], [preImage]); - - clientA.conditionalTransfer({ - amount: transfer.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transfer.assetId, - meta: { foo: "bar", sender: clientA.publicIdentifier }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer); - await new Promise((res) => clientB.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, res)); + await sendHashlockTransfer(clientA, clientB, opts); const badPreImage = getRandomBytes32(); await expect( @@ -402,30 +323,13 @@ describe("HashLock Transfers", () => { // timelock variable it("cannot resolve a hashlock if timelock is expired", async () => { const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; - await fundChannel(clientA, transfer.amount, transfer.assetId); const preImage = getRandomBytes32(); - const timelock = 101; + const timelock = (101).toString(); + const opts = { ...transfer, preImage, timelock }; - const lockHash = soliditySha256(["bytes32"], [preImage]); - await new Promise((resolve, reject) => { - clientA.conditionalTransfer({ - amount: transfer.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transfer.assetId, - meta: { foo: "bar", sender: clientA.publicIdentifier }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer); - clientB.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, resolve); - clientA.once(EventNames.REJECT_INSTALL_EVENT, reject); - }); + await sendHashlockTransfer(clientA, clientB, opts); - // Wait for more than one block if blocktime > 0 - // for (let i = 0; i < 5; i++) { - // eslint-disable-next-line no-loop-func await new Promise((resolve) => provider.once("block", resolve)); - // } await expect( clientB.resolveCondition({ @@ -470,75 +374,7 @@ describe("HashLock Transfers", () => { ).to.be.fulfilled; }); - it.skip("Experimental: Average latency of 5 hashlock transfers with Eth", async () => { - const runTime: number[] = []; - let sum = 0; - const numberOfRuns = 5; - const transfer: AssetOptions = { amount: ETH_AMOUNT_SM, assetId: AddressZero }; - await fundChannel(clientA, transfer.amount.mul(25), transfer.assetId); - await requestCollateral(clientB, transfer.assetId); - - for (let i = 0; i < numberOfRuns; i++) { - const { - [clientA.signerAddress]: clientAPreBal, - [clientA.nodeSignerAddress]: nodeAPreBal, - } = await clientA.getFreeBalance(transfer.assetId); - const { - [clientB.signerAddress]: clientBPreBal, - [clientB.nodeSignerAddress]: nodeBPreBal, - } = await clientB.getFreeBalance(transfer.assetId); - - const preImage = getRandomBytes32(); - const timelock = (5000).toString(); - const lockHash = soliditySha256(["bytes32"], [preImage]); - - // Start timer - const start = Date.now(); - - // both sender + receiver apps installed, sender took action - clientA.conditionalTransfer({ - amount: transfer.amount.toString(), - conditionType: ConditionalTransferTypes.HashLockTransfer, - lockHash, - timelock, - assetId: transfer.assetId, - meta: { foo: "bar", sender: clientA.publicIdentifier }, - recipient: clientB.publicIdentifier, - } as PublicParams.HashLockTransfer); - - // eslint-disable-next-line no-loop-func - await new Promise((res) => clientB.once(EventNames.CONDITIONAL_TRANSFER_CREATED_EVENT, res)); - - // eslint-disable-next-line no-loop-func - await new Promise(async (res) => { - clientA.once(EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, async (data) => { - res(); - }); - await clientB.resolveCondition({ - conditionType: ConditionalTransferTypes.HashLockTransfer, - preImage, - assetId: transfer.assetId, - } as PublicParams.ResolveHashLockTransfer); - }); + it("should be able to cancel an active payment", async () => {}); - // Stop timer and add to sum - runTime[i] = Date.now() - start; - console.log(`Run: ${i}, Runtime: ${runTime[i]}`); - sum = sum + runTime[i]; - - const { - [clientA.signerAddress]: clientAPostBal, - [clientA.nodeSignerAddress]: nodeAPostBal, - } = await clientA.getFreeBalance(transfer.assetId); - const { - [clientB.signerAddress]: clientBPostBal, - [clientB.nodeSignerAddress]: nodeBPostBal, - } = await clientB.getFreeBalance(transfer.assetId); - expect(clientAPostBal).to.eq(clientAPreBal.sub(transfer.amount)); - expect(nodeAPostBal).to.eq(nodeAPreBal.add(transfer.amount)); - expect(nodeBPostBal).to.eq(nodeBPreBal.sub(transfer.amount)); - expect(clientBPostBal).to.eq(clientBPreBal.add(transfer.amount)); - } - console.log(`Average = ${sum / numberOfRuns} ms`); - }); + it("should be able to refund an expired payment", async () => {}); }); From c605e769bb2f4984cf1c543e8c317ee9a31b92a8 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 11:32:31 -0700 Subject: [PATCH 04/36] prettier --- .../src.sol/apps/HashLockTransferApp.sol | 128 ++++++++---------- 1 file changed, 58 insertions(+), 70 deletions(-) diff --git a/modules/contracts/src.sol/apps/HashLockTransferApp.sol b/modules/contracts/src.sol/apps/HashLockTransferApp.sol index 4a16f87c04..19b6a5fdd2 100644 --- a/modules/contracts/src.sol/apps/HashLockTransferApp.sol +++ b/modules/contracts/src.sol/apps/HashLockTransferApp.sol @@ -5,89 +5,77 @@ pragma experimental "ABIEncoderV2"; import "../adjudicator/interfaces/CounterfactualApp.sol"; import "../funding/libs/LibOutcome.sol"; - /// @title Lightning HTLC Transfer App /// @notice This contract allows users to claim a payment locked in /// the application if they provide a preImage and expiry /// that corresponds to a lightning htlc contract HashLockTransferApp is CounterfactualApp { + /** + * This app can also not be used to send _multiple_ hashlocked payments, + * only one can be redeemed with the preImage. + */ + struct AppState { + LibOutcome.CoinTransfer[2] coinTransfers; + bytes32 lockHash; + bytes32 preImage; + uint256 expiry; + bool finalized; + } - /** - * This app can also not be used to send _multiple_ hashlocked payments, - * only one can be redeemed with the preImage. - */ - struct AppState { - LibOutcome.CoinTransfer[2] coinTransfers; - bytes32 lockHash; - bytes32 preImage; - uint256 expiry; - bool finalized; - } + struct Action { + bytes32 preImage; + } - struct Action { - bytes32 preImage; - } + function applyAction(bytes calldata encodedState, bytes calldata encodedAction) + external + override + view + returns (bytes memory) + { + AppState memory state = abi.decode(encodedState, (AppState)); + Action memory action = abi.decode(encodedAction, (Action)); + bytes32 generatedHash = sha256(abi.encode(action.preImage)); - function applyAction( - bytes calldata encodedState, - bytes calldata encodedAction - ) - override - external - view - returns (bytes memory) - { - AppState memory state = abi.decode(encodedState, (AppState)); - Action memory action = abi.decode(encodedAction, (Action)); - bytes32 generatedHash = sha256(abi.encode(action.preImage)); + require(!state.finalized, "Cannot take action on finalized state"); + require(block.number < state.expiry, "Cannot take action if expiry is expired"); + require( + state.lockHash == generatedHash, + "Hash generated from preimage does not match hash in state" + ); - require(!state.finalized, "Cannot take action on finalized state"); - require(block.number < state.expiry, "Cannot take action if expiry is expired"); - require(state.lockHash == generatedHash, "Hash generated from preimage does not match hash in state"); + state.coinTransfers[1].amount = state.coinTransfers[0].amount; + state.coinTransfers[0].amount = 0; + state.preImage = action.preImage; + state.finalized = true; - state.coinTransfers[1].amount = state.coinTransfers[0].amount; - state.coinTransfers[0].amount = 0; - state.preImage = action.preImage; - state.finalized = true; - - return abi.encode(state); - } + return abi.encode(state); + } - function computeOutcome(bytes calldata encodedState) - override - external - view - returns (bytes memory) - { - AppState memory state = abi.decode(encodedState, (AppState)); + function computeOutcome(bytes calldata encodedState) + external + override + view + returns (bytes memory) + { + AppState memory state = abi.decode(encodedState, (AppState)); - // If payment hasn't been unlocked, require that the expiry is expired - if (!state.finalized) { - require(block.number >= state.expiry, "Cannot revert payment if expiry is unexpired"); - } - - return abi.encode(state.coinTransfers); + // If payment hasn't been unlocked, require that the expiry is expired + if (!state.finalized) { + require(block.number >= state.expiry, "Cannot revert payment if expiry is unexpired"); } - function getTurnTaker( - bytes calldata /* encodedState */, - address[] calldata participants - ) - override - external - view - returns (address) - { - return participants[1]; // receiver should always be indexed at [1] - } + return abi.encode(state.coinTransfers); + } - function isStateTerminal(bytes calldata encodedState) - override - external - view - returns (bool) - { - AppState memory state = abi.decode(encodedState, (AppState)); - return state.finalized; - } + function getTurnTaker( + bytes calldata, /* encodedState */ + address[] calldata participants + ) external override view returns (address) { + return participants[1]; // receiver should always be indexed at [1] + } + + function isStateTerminal(bytes calldata encodedState) external override view returns (bool) { + AppState memory state = abi.decode(encodedState, (AppState)); + return state.finalized; + } } From 7d9a96cc5569d6443964f88778a61f9007e3a992 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 11:34:03 -0700 Subject: [PATCH 05/36] wrong secret still finalized --- .../contracts/src.sol/apps/HashLockTransferApp.sol | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/contracts/src.sol/apps/HashLockTransferApp.sol b/modules/contracts/src.sol/apps/HashLockTransferApp.sol index 19b6a5fdd2..073ce4362c 100644 --- a/modules/contracts/src.sol/apps/HashLockTransferApp.sol +++ b/modules/contracts/src.sol/apps/HashLockTransferApp.sol @@ -38,14 +38,12 @@ contract HashLockTransferApp is CounterfactualApp { require(!state.finalized, "Cannot take action on finalized state"); require(block.number < state.expiry, "Cannot take action if expiry is expired"); - require( - state.lockHash == generatedHash, - "Hash generated from preimage does not match hash in state" - ); - - state.coinTransfers[1].amount = state.coinTransfers[0].amount; - state.coinTransfers[0].amount = 0; - state.preImage = action.preImage; + if (state.lockHash == generatedHash) { + // correct preimage, send payment to receiver + state.coinTransfers[1].amount = state.coinTransfers[0].amount; + state.coinTransfers[0].amount = 0; + state.preImage = action.preImage; + } state.finalized = true; return abi.encode(state); From 5e69e1c09dcff6c767112c23b47f312261804ec2 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 11:38:27 -0700 Subject: [PATCH 06/36] create test --- .../tests/apps/hashlock-transfer-app.spec.ts | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts b/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts index 87ce47c226..6d12004ccb 100644 --- a/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts +++ b/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts @@ -14,7 +14,7 @@ import { HashLockTransferApp } from "../../artifacts"; import { expect, provider } from "../utils"; -const { Zero } = constants; +const { Zero, HashZero } = constants; const { defaultAbiCoder, soliditySha256 } = utils; const decodeTransfers = (encodedAppState: string): CoinTransfer[] => @@ -137,14 +137,41 @@ describe("HashLockTransferApp", () => { validateOutcome(ret, expectedPostState); }); - it("will revert action with incorrect hash", async () => { + it("will not redeem a payment if an incorrect hash is given", async () => { const action: HashLockTransferAppAction = { preImage: getRandomBytes32(), // incorrect hash }; - await expect(applyAction(preState, action)).revertedWith( - "Hash generated from preimage does not match hash in state", + let ret = await applyAction(preState, action); + const afterActionState = decodeAppState(ret); + + const expectedPostState: HashLockTransferAppState = { + coinTransfers: [ + { + amount: transferAmount, + to: senderAddr, + }, + { + amount: Zero, + to: receiverAddr, + }, + ], + lockHash, + preImage: HashZero, + expiry, + finalized: true, + }; + + expect(afterActionState.finalized).to.eq(expectedPostState.finalized); + expect(afterActionState.coinTransfers[0].amount).to.eq( + expectedPostState.coinTransfers[0].amount, + ); + expect(afterActionState.coinTransfers[1].amount).to.eq( + expectedPostState.coinTransfers[1].amount, ); + + ret = await computeOutcome(afterActionState); + validateOutcome(ret, expectedPostState); }); it("will revert action if already finalized", async () => { From b0603a3512e1873114263cc5493a5ae582a87145 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 12:07:10 -0700 Subject: [PATCH 07/36] enforce turn taker at app level --- modules/cf-core/src/models/app-instance.ts | 42 +++++++++++++++++++ modules/cf-core/src/protocol/sync.ts | 4 +- modules/cf-core/src/protocol/take-action.ts | 13 +++--- modules/cf-core/src/protocol/uninstall.ts | 3 ++ .../scenarios/uninstall-with-action.spec.ts | 11 ++++- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/modules/cf-core/src/models/app-instance.ts b/modules/cf-core/src/models/app-instance.ts index f4a94096c5..e5274f73ea 100644 --- a/modules/cf-core/src/models/app-instance.ts +++ b/modules/cf-core/src/models/app-instance.ts @@ -293,12 +293,54 @@ export class AppInstance { return this.computeOutcome(this.state, provider, bytecode); } + public async computeTurnTaker( + provider: providers.JsonRpcProvider, + bytecode?: HexString, + ): Promise { + let turnTaker: undefined | string = undefined; + // attempt evm if available + if (bytecode) { + try { + const functionData = appInterface.encodeFunctionData("getTurnTaker", [ + this.encodedLatestState, + this.participants, + ]); + const output = await execEvmBytecode(bytecode, functionData); + turnTaker = appInterface.decodeFunctionResult("getTurnTaker", output)[0]; + } catch (e) {} + } + if (turnTaker) { + return turnTaker; + } + // otherwise, if err or if no bytecode, execute read fn + turnTaker = (await this.toEthersContract(provider).getTurnTaker( + this.encodedLatestState, + this.participants, + )) as string; + return turnTaker; + } + + public async isCorrectTurnTaker( + attemptedTurnTaker: string, + provider: providers.JsonRpcProvider, + bytecode?: HexString, + ) { + const turnTaker = await this.computeTurnTaker(provider, bytecode); + return attemptedTurnTaker === turnTaker; + } + public async computeStateTransition( + actionTaker: Address, action: SolidityValueType, provider: providers.JsonRpcProvider, bytecode?: HexString, ): Promise { let computedNextState: SolidityValueType; + if (!(await this.isCorrectTurnTaker(actionTaker, provider, bytecode))) { + throw new Error( + `Cannot compute state transition, got invalid turn taker (${actionTaker}) for action on app at ${this.appDefinition}`, + ); + } if (bytecode) { try { const functionData = appInterface.encodeFunctionData("applyAction", [ diff --git a/modules/cf-core/src/protocol/sync.ts b/modules/cf-core/src/protocol/sync.ts index 859a07f736..3b4ef03161 100644 --- a/modules/cf-core/src/protocol/sync.ts +++ b/modules/cf-core/src/protocol/sync.ts @@ -11,7 +11,7 @@ import { AppInstanceJson, } from "@connext/types"; import { Context, ProtocolExecutionFlow, PersistStateChannelType } from "../types"; -import { stringify, logTime, toBN } from "@connext/utils"; +import { stringify, logTime, toBN, getSignerAddressFromPublicIdentifier } from "@connext/utils"; import { stateChannelClassFromStoreByMultisig, getPureBytecode } from "./utils"; import { StateChannel, AppInstance, FreeBalanceClass } from "../models"; import { @@ -182,6 +182,7 @@ export const SYNC_PROTOCOL: ProtocolExecutionFlow = { postSyncStateChannel = postSyncStateChannel.setState( app, await app.computeStateTransition( + getSignerAddressFromPublicIdentifier(initiatorIdentifier), affectedApp!.latestAction, provider, getPureBytecode(app.appDefinition, contractAddresses), @@ -391,6 +392,7 @@ export const SYNC_PROTOCOL: ProtocolExecutionFlow = { postSyncStateChannel = postSyncStateChannel.setState( app, await app.computeStateTransition( + getSignerAddressFromPublicIdentifier(initiatorIdentifier), affectedApp!.latestAction, provider, getPureBytecode(app.appDefinition, contractAddresses), diff --git a/modules/cf-core/src/protocol/take-action.ts b/modules/cf-core/src/protocol/take-action.ts index 0c66426544..df6d8d92e6 100644 --- a/modules/cf-core/src/protocol/take-action.ts +++ b/modules/cf-core/src/protocol/take-action.ts @@ -38,6 +38,7 @@ export const TAKE_ACTION_PROTOCOL: ProtocolExecutionFlow = { responderIdentifier, action, stateTimeout, + initiatorIdentifier, } = params as ProtocolParams.TakeAction; if (!preProtocolStateChannel) { @@ -67,12 +68,10 @@ export const TAKE_ACTION_PROTOCOL: ProtocolExecutionFlow = { const postProtocolStateChannel = preProtocolStateChannel.setState( preAppInstance, await preAppInstance.computeStateTransition( + getSignerAddressFromPublicIdentifier(initiatorIdentifier), action, network.provider, - getPureBytecode( - preAppInstance.appDefinition, - network.contractAddresses, - ), + getPureBytecode(preAppInstance.appDefinition, network.contractAddresses), ), stateTimeout, ); @@ -200,12 +199,10 @@ export const TAKE_ACTION_PROTOCOL: ProtocolExecutionFlow = { const postProtocolStateChannel = preProtocolStateChannel.setState( preAppInstance, await preAppInstance.computeStateTransition( + getSignerAddressFromPublicIdentifier(initiatorIdentifier), action, network.provider, - getPureBytecode( - preAppInstance.appDefinition, - network.contractAddresses, - ), + getPureBytecode(preAppInstance.appDefinition, network.contractAddresses), ), stateTimeout, ); diff --git a/modules/cf-core/src/protocol/uninstall.ts b/modules/cf-core/src/protocol/uninstall.ts index dde3a8601c..35639a8544 100644 --- a/modules/cf-core/src/protocol/uninstall.ts +++ b/modules/cf-core/src/protocol/uninstall.ts @@ -42,6 +42,7 @@ export const UNINSTALL_PROTOCOL: ProtocolExecutionFlow = { appIdentityHash, action, stateTimeout, + initiatorIdentifier, } = params as ProtocolParams.Uninstall; if (!preProtocolStateChannel) { @@ -70,6 +71,7 @@ export const UNINSTALL_PROTOCOL: ProtocolExecutionFlow = { // apply action substart = Date.now(); const newState = await appToUninstall.computeStateTransition( + getSignerAddressFromPublicIdentifier(initiatorIdentifier), action, network.provider, getPureBytecode(appToUninstall.appDefinition, network.contractAddresses), @@ -205,6 +207,7 @@ export const UNINSTALL_PROTOCOL: ProtocolExecutionFlow = { // apply action substart = Date.now(); const newState = await appToUninstall.computeStateTransition( + getSignerAddressFromPublicIdentifier(initiatorIdentifier), action, network.provider, getPureBytecode(appToUninstall.appDefinition, network.contractAddresses), diff --git a/modules/cf-core/src/testing/scenarios/uninstall-with-action.spec.ts b/modules/cf-core/src/testing/scenarios/uninstall-with-action.spec.ts index 7eb17d926c..6ac588b541 100644 --- a/modules/cf-core/src/testing/scenarios/uninstall-with-action.spec.ts +++ b/modules/cf-core/src/testing/scenarios/uninstall-with-action.spec.ts @@ -9,6 +9,7 @@ import { SimpleLinkedTransferAppName, AppActions, } from "@connext/types"; +import { getRandomBytes32, getSignerAddressFromPublicIdentifier } from "@connext/utils"; import { constants, utils } from "ethers"; import { CFCore } from "../../cfCore"; @@ -26,7 +27,6 @@ import { getAppInstance, } from "../utils"; import { AppInstance } from "../../models"; -import { getRandomBytes32 } from "@connext/utils"; import { expect } from "../assertions"; const { One, Two, Zero, HashZero } = constants; @@ -100,7 +100,14 @@ describe("Node A and B install an app, then uninstall with a given action", () = ); const appPreUninstall = AppInstance.fromJson(await getAppInstance(nodeA, appIdentityHash)); const expected = appPreUninstall - .setState(await appPreUninstall.computeStateTransition(action, provider), Zero) + .setState( + await appPreUninstall.computeStateTransition( + getSignerAddressFromPublicIdentifier(nodeA.publicIdentifier), + action, + provider, + ), + Zero, + ) .toJson(); await Promise.all([ From 38cfa0b9f4d750adc6eecdf9abbcb0a89be35c66 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 12:07:16 -0700 Subject: [PATCH 08/36] add turntaker test --- .../src.ts/tests/apps/hashlock-transfer-app.spec.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts b/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts index 6d12004ccb..f7db9d53c3 100644 --- a/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts +++ b/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts @@ -99,7 +99,17 @@ describe("HashLockTransferApp", () => { }; }); - describe("update state", () => { + describe("getTurnTaker", () => { + it("will return payment recipient", async () => { + const ret = await hashLockTransferApp.getTurnTaker(encodeAppState(preState), [ + senderAddr, + receiverAddr, + ]); + expect(ret).to.be.eq(receiverAddr); + }); + }); + + describe("applyAction", () => { it("will redeem a payment with correct hash within expiry", async () => { const action: HashLockTransferAppAction = { preImage, From 7973319a7ae0af422cb91e8d4ba6d5b6fe0ead88 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 12:10:33 -0700 Subject: [PATCH 09/36] fix failure message --- modules/cf-core/src/testing/scenarios/cant-take-action.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/cf-core/src/testing/scenarios/cant-take-action.spec.ts b/modules/cf-core/src/testing/scenarios/cant-take-action.spec.ts index 4b227d4174..41eb69591c 100644 --- a/modules/cf-core/src/testing/scenarios/cant-take-action.spec.ts +++ b/modules/cf-core/src/testing/scenarios/cant-take-action.spec.ts @@ -1,5 +1,4 @@ import { CFCore } from "../../cfCore"; -import { INVALID_ACTION } from "../../errors"; import { TestContractAddresses } from "../contracts"; import { constructTakeActionRpc, createChannel, installApp } from "../utils"; @@ -35,7 +34,7 @@ describe("Node method follows spec - fails with improper action taken", () => { const takeActionReq = constructTakeActionRpc(appIdentityHash, multisigAddress, validAction); await expect(nodeA.rpcRouter.dispatch(takeActionReq)).to.eventually.be.rejectedWith( - INVALID_ACTION, + "Cannot compute state transition", ); }); }); From 6f5b30693637cae92e0f624c1448092dd67d5ce9 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 12:22:08 -0700 Subject: [PATCH 10/36] improve error --- modules/cf-core/src/models/app-instance.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/cf-core/src/models/app-instance.ts b/modules/cf-core/src/models/app-instance.ts index e5274f73ea..6637cd59ec 100644 --- a/modules/cf-core/src/models/app-instance.ts +++ b/modules/cf-core/src/models/app-instance.ts @@ -336,9 +336,10 @@ export class AppInstance { bytecode?: HexString, ): Promise { let computedNextState: SolidityValueType; - if (!(await this.isCorrectTurnTaker(actionTaker, provider, bytecode))) { + const turnTaker = await this.computeTurnTaker(provider, bytecode); + if (actionTaker !== turnTaker) { throw new Error( - `Cannot compute state transition, got invalid turn taker (${actionTaker}) for action on app at ${this.appDefinition}`, + `Cannot compute state transition, got invalid turn taker for action on app at ${this.appDefinition}. Expected ${turnTaker}, got ${actionTaker}`, ); } if (bytecode) { From bd208b0fa3f0e9188a7fd8c1874a7b74e315b673 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 12:22:22 -0700 Subject: [PATCH 11/36] use correct turn taker in test --- .../src/testing/scenarios/take-action-concurrent.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/cf-core/src/testing/scenarios/take-action-concurrent.spec.ts b/modules/cf-core/src/testing/scenarios/take-action-concurrent.spec.ts index cc32175ed4..8163984420 100644 --- a/modules/cf-core/src/testing/scenarios/take-action-concurrent.spec.ts +++ b/modules/cf-core/src/testing/scenarios/take-action-concurrent.spec.ts @@ -76,15 +76,15 @@ describe("Node method follows spec - toke action", () => { let appsTakenActionOn = 0; - nodeB.on(EventNames.UPDATE_STATE_EVENT, () => { + nodeA.on(EventNames.UPDATE_STATE_EVENT, () => { appsTakenActionOn += 1; if (appsTakenActionOn === 2) done(); }); - nodeA.rpcRouter.dispatch( + nodeB.rpcRouter.dispatch( constructTakeActionRpc(appIdentityHashes[0], multisigAddress, validAction), ); - nodeA.rpcRouter.dispatch( + nodeB.rpcRouter.dispatch( constructTakeActionRpc(appIdentityHashes[1], multisigAddress, validAction), ); }); From b803560ae05ba7e095c65b1f0072abc729bf2ccd Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 12:28:25 -0700 Subject: [PATCH 12/36] fix turn taker in test --- modules/cf-core/src/testing/scenarios/take-action.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cf-core/src/testing/scenarios/take-action.spec.ts b/modules/cf-core/src/testing/scenarios/take-action.spec.ts index d14a399408..745271e3de 100644 --- a/modules/cf-core/src/testing/scenarios/take-action.spec.ts +++ b/modules/cf-core/src/testing/scenarios/take-action.spec.ts @@ -122,7 +122,7 @@ describe("Node method follows spec - takeAction", () => { result: { result: { newState }, }, - } = await nodeA.rpcRouter.dispatch(takeActionReq); + } = await nodeB.rpcRouter.dispatch(takeActionReq); // allow nodeA to confirm its messages await new Promise((resolve) => { nodeA.once(EventNames.UPDATE_STATE_EVENT, () => { From a1a4634f9985e197232eaa611bd1bcbaa6c9e556 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 12:40:12 -0700 Subject: [PATCH 13/36] fix turn taker in linked transfer test --- .../src/testing/scenarios/uninstall-with-action.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/cf-core/src/testing/scenarios/uninstall-with-action.spec.ts b/modules/cf-core/src/testing/scenarios/uninstall-with-action.spec.ts index 6ac588b541..0a93e94fbe 100644 --- a/modules/cf-core/src/testing/scenarios/uninstall-with-action.spec.ts +++ b/modules/cf-core/src/testing/scenarios/uninstall-with-action.spec.ts @@ -102,7 +102,7 @@ describe("Node A and B install an app, then uninstall with a given action", () = const expected = appPreUninstall .setState( await appPreUninstall.computeStateTransition( - getSignerAddressFromPublicIdentifier(nodeA.publicIdentifier), + getSignerAddressFromPublicIdentifier(nodeB.publicIdentifier), action, provider, ), @@ -112,13 +112,13 @@ describe("Node A and B install an app, then uninstall with a given action", () = await Promise.all([ new Promise(async (resolve, reject) => { - nodeB.on(EventNames.UNINSTALL_EVENT, async (msg) => { + nodeA.on(EventNames.UNINSTALL_EVENT, async (msg) => { if (msg.data.appIdentityHash !== appIdentityHash) { return; } try { assertUninstallMessage( - nodeA.publicIdentifier, + nodeB.publicIdentifier, multisigAddress, appIdentityHash, expected, @@ -138,7 +138,7 @@ describe("Node A and B install an app, then uninstall with a given action", () = }), new Promise(async (resolve, reject) => { try { - await nodeA.rpcRouter.dispatch( + await nodeB.rpcRouter.dispatch( constructUninstallRpc(appIdentityHash, multisigAddress, action), ); From 081e7232f93ad43909eb9aa8731367dbe4a87e27 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 21:47:40 -0700 Subject: [PATCH 14/36] client side working --- modules/client/src/connext.ts | 103 +++++++------- .../controllers/ResolveTransferController.ts | 131 +++++++++++++++++- modules/types/src/public.ts | 14 +- 3 files changed, 189 insertions(+), 59 deletions(-) diff --git a/modules/client/src/connext.ts b/modules/client/src/connext.ts index 5f0327d8c1..98e516a1a3 100644 --- a/modules/client/src/connext.ts +++ b/modules/client/src/connext.ts @@ -325,12 +325,6 @@ export class ConnextClient implements IConnextClient { public resolveCondition = async ( params: PublicParams.ResolveCondition, ): Promise => { - // paymentId is generated for hashlock transfer - if (params.conditionType === ConditionalTransferTypes.HashLockTransfer && !params.paymentId) { - const lockHash = soliditySha256(["bytes32"], [params.preImage]); - const paymentId = soliditySha256(["address", "bytes32"], [params.assetId, lockHash]); - params.paymentId = paymentId; - } return this.resolveTransferController.resolveTransfer(params); }; @@ -378,7 +372,11 @@ export class ConnextClient implements IConnextClient { // object might not be in the store yet. We should still wait for at least 1 withdrawal const withdrawalsToFind = (await this.getUserWithdrawals()).length || 1; - this.log.info(`Watching for ${withdrawalsToFind} withdrawal${withdrawalsToFind === 1 ? "s" : ""} starting at block ${startingBlock}`); + this.log.info( + `Watching for ${withdrawalsToFind} withdrawal${ + withdrawalsToFind === 1 ? "s" : "" + } starting at block ${startingBlock}`, + ); const getTransactionResponse = async ( tx: MinimalTransaction, @@ -424,17 +422,39 @@ export class ConnextClient implements IConnextClient { return responses; }; - return new Promise(async (resolve: any, reject: any): Promise => { - - // First, start listener & process the next n blocks. If no withdrawal found, reject. - this.ethProvider.on( - "block", - async (blockNumber: number): Promise => { - // in the `WithdrawalController` the user does not store the - // commitment until `takeAction` happens, so this may be 0 - // meaning the withdrawal has not been saved to the store yet - (await checkForUserWithdrawals(blockNumber)).forEach(async ([storedValue, tx]) => { - if (tx) { // && !transactions.some(t => t.hash === tx.hash)) { + return new Promise( + async (resolve: any, reject: any): Promise => { + // First, start listener & process the next n blocks. If no withdrawal found, reject. + this.ethProvider.on( + "block", + async (blockNumber: number): Promise => { + // in the `WithdrawalController` the user does not store the + // commitment until `takeAction` happens, so this may be 0 + // meaning the withdrawal has not been saved to the store yet + (await checkForUserWithdrawals(blockNumber)).forEach(async ([storedValue, tx]) => { + if (tx) { + // && !transactions.some(t => t.hash === tx.hash)) { + this.log.info(`Found new tx at block ${tx.blockNumber} for withdrawal: ${tx.hash}`); + transactions.push(tx); + await this.channelProvider.send(ChannelMethods.chan_setUserWithdrawal, { + withdrawalObject: storedValue, + remove: true, + }); + } + }); + if (blockNumber - startingBlock > blocksAhead) { + this.ethProvider.removeAllListeners("block"); + return reject(`More than ${blocksAhead} have passed`); + } + }, + ); + + // Second, look for withdrawals in the previous n blocks + for (let i = 0; i < blocksBehind; i++) { + // eslint-disable-next-line no-loop-func + (await checkForUserWithdrawals(startingBlock - i)).forEach(async ([storedValue, tx]) => { + if (tx) { + // && !transactions.some(t => t.hash === tx.hash)) { this.log.info(`Found new tx at block ${tx.blockNumber} for withdrawal: ${tx.hash}`); transactions.push(tx); await this.channelProvider.send(ChannelMethods.chan_setUserWithdrawal, { @@ -443,41 +463,23 @@ export class ConnextClient implements IConnextClient { }); } }); - if (blockNumber - startingBlock > blocksAhead) { - this.ethProvider.removeAllListeners("block"); - return reject(`More than ${blocksAhead} have passed`); - } - }, - ); + } - // Second, look for withdrawals in the previous n blocks - for (let i = 0; i < blocksBehind; i++) { - // eslint-disable-next-line no-loop-func - (await checkForUserWithdrawals(startingBlock - i)).forEach(async ([storedValue, tx]) => { - if (tx) { // && !transactions.some(t => t.hash === tx.hash)) { - this.log.info(`Found new tx at block ${tx.blockNumber} for withdrawal: ${tx.hash}`); - transactions.push(tx); - await this.channelProvider.send(ChannelMethods.chan_setUserWithdrawal, { - withdrawalObject: storedValue, - remove: true, - }); + // Third, wait until the previous two steps have found all the withdrawals + while (true) { + const withdrawals = await this.getUserWithdrawals(); + if (transactions.length > 0 && withdrawals.length < 1) { + this.log.info( + `Found ${transactions.length} transactions, done looking for withdrawals`, + ); + this.ethProvider.removeAllListeners("block"); + return resolve(transactions); + } else { + await delay(500); } - }); - } - - // Third, wait until the previous two steps have found all the withdrawals - while (true) { - const withdrawals = await this.getUserWithdrawals(); - if (transactions.length > 0 && withdrawals.length < 1) { - this.log.info(`Found ${transactions.length} transactions, done looking for withdrawals`); - this.ethProvider.removeAllListeners("block"); - return resolve(transactions); - } else { - await delay(500); } - } - - }); + }, + ); }; //////////////////////////////////////// @@ -995,5 +997,4 @@ export class ConnextClient implements IConnextClient { } return undefined; }; - } diff --git a/modules/client/src/controllers/ResolveTransferController.ts b/modules/client/src/controllers/ResolveTransferController.ts index ebec39e5d0..fdf3e01f99 100644 --- a/modules/client/src/controllers/ResolveTransferController.ts +++ b/modules/client/src/controllers/ResolveTransferController.ts @@ -10,8 +10,9 @@ import { SimpleLinkedTransferAppAction, GraphSignedTransferAppAction, AppInstanceJson, + HashLockTransferAppState, } from "@connext/types"; -import { stringify } from "@connext/utils"; +import { stringify, getRandomBytes32, toBN } from "@connext/utils"; import { BigNumber } from "ethers"; import { AbstractController } from "./AbstractController"; @@ -48,6 +49,20 @@ export class ResolveTransferController extends AbstractController { return; }; + // Extract the secret object from the params; + if (!this.hasSecret(params)) { + // User is cancelling the payment + try { + console.log(`trying to cancel payment`); + const ret = await this.handleCancellation(params); + this.log.info(`[${paymentId}] resolveCondition complete: ${stringify(ret)}`); + return ret; + } catch (e) { + emitFailureEvent(e); + throw e; + } + } + // Install app with receiver let appIdentityHash: string; let amount: BigNumber; @@ -210,4 +225,118 @@ export class ResolveTransferController extends AbstractController { this.log.info(`[${paymentId}] resolveCondition complete: ${stringify(result)}`); return result; }; + + // Helper functions + private hasSecret(params: PublicParams.ResolveCondition): boolean { + const { conditionType, paymentId } = params; + switch (conditionType) { + case ConditionalTransferTypes.HashLockTransfer: { + const { preImage } = params as PublicParams.ResolveHashLockTransfer; + return !!preImage; + } + case ConditionalTransferTypes.GraphTransfer: { + const { responseCID, signature } = params as PublicParams.ResolveGraphTransfer; + return !!responseCID && !!signature; + } + case ConditionalTransferTypes.SignedTransfer: { + const { data, signature } = params as PublicParams.ResolveSignedTransfer; + return !!data && !!signature; + } + case ConditionalTransferTypes.LinkedTransfer: { + const { preImage } = params as PublicParams.ResolveLinkedTransfer; + return !!preImage; + } + default: { + const c: never = conditionType; + this.log.error(`[${paymentId}] Unsupported conditionType ${c}`); + } + } + throw new Error(`Invalid condition type: ${conditionType}`); + } + + private async handleCancellation( + params: PublicParams.ResolveCondition, + ): Promise { + const { conditionType, paymentId } = params; + const appDefinition = this.connext.appRegistry.find((app) => app.name === conditionType) + .appDefinitionAddress; + const apps = await this.connext.getAppInstances(); + const paymentApp = apps.find((app) => { + const participants = (app.latestState as GenericConditionalTransferAppState).coinTransfers.map( + (t) => t.to, + ); + return ( + app.appDefinition === appDefinition && + app.meta.paymentId === paymentId && + participants.includes(this.connext.signerAddress) + ); + }); + + if (!paymentApp) { + throw new Error(`Cannot find payment associated with ${paymentId}`); + } + + const ret = { + appIdentityHash: paymentApp.identityHash, + amount: (paymentApp.latestState as GenericConditionalTransferAppState).coinTransfers[0] + .amount, + assetId: paymentApp.outcomeInterpreterParameters["tokenAddress"], + meta: paymentApp.meta, + paymentId: params.paymentId, + sender: paymentApp.meta.sender, + }; + + switch (conditionType) { + case ConditionalTransferTypes.HashLockTransfer: { + // if it is the sender app, can only cancel if the app has expired + const state = paymentApp.latestState as HashLockTransferAppState; + const isSender = state.coinTransfers[0].to === this.connext.signerAddress; + + if (isSender) { + // uninstall app + this.log.info( + `[${paymentId}] Uninstalling transfer app without action ${paymentApp.identityHash}`, + ); + await this.connext.uninstallApp(paymentApp.identityHash); + this.log.info( + `[${paymentId}] Finished uninstalling transfer app ${paymentApp.identityHash}`, + ); + return ret; + } + + let action = undefined; + if (toBN(await this.ethProvider.getBlockNumber()).lt(toBN(state.expiry))) { + // uninstall with bad action iff the app is active, otherwise just + // uninstall + action = { preImage: getRandomBytes32() }; + } + this.log.info( + `[${paymentId}] Uninstalling transfer app with empty action ${paymentApp.identityHash}`, + ); + console.log(`uninstalling payment with action`, action); + await this.connext.uninstallApp(paymentApp.identityHash, action); + this.log.info( + `[${paymentId}] Finished uninstalling transfer app ${paymentApp.identityHash}`, + ); + return ret; + } + case ConditionalTransferTypes.GraphTransfer: + case ConditionalTransferTypes.SignedTransfer: + case ConditionalTransferTypes.LinkedTransfer: { + // uninstall the app without taking action + this.log.info( + `[${paymentId}] Uninstalling transfer app without action ${paymentApp.identityHash}`, + ); + await this.connext.uninstallApp(paymentApp.identityHash); + this.log.info( + `[${paymentId}] Finished uninstalling transfer app ${paymentApp.identityHash}`, + ); + return ret; + } + default: { + const c: never = conditionType; + throw new Error(`Unable to cancel payment, unsupported condition ${c}`); + } + } + } } diff --git a/modules/types/src/public.ts b/modules/types/src/public.ts index 66ad2c64be..c37a18f1ab 100644 --- a/modules/types/src/public.ts +++ b/modules/types/src/public.ts @@ -53,8 +53,8 @@ type HashLockTransferResponse = { type ResolveHashLockTransferParameters = { conditionType: typeof ConditionalTransferTypes.HashLockTransfer; assetId: Address; - paymentId?: Bytes32; - preImage: Bytes32; + paymentId: Bytes32; + preImage?: Bytes32; }; type ResolveHashLockTransferResponse = { @@ -87,7 +87,7 @@ type LinkedTransferResponse = { type ResolveLinkedTransferParameters = { conditionType: typeof ConditionalTransferTypes.LinkedTransfer; paymentId: Bytes32; - preImage: Bytes32; + preImage?: Bytes32; }; type ResolveLinkedTransferResponse = { @@ -122,8 +122,8 @@ type SignedTransferResponse = { type ResolveSignedTransferParameters = { conditionType: typeof ConditionalTransferTypes.SignedTransfer; paymentId: Bytes32; - data: Bytes32; - signature: SignatureString; + data?: Bytes32; + signature?: SignatureString; }; type ResolveSignedTransferResponse = { @@ -159,8 +159,8 @@ type GraphSignedTransferResponse = { type ResolveGraphSignedTransferParameters = { conditionType: typeof ConditionalTransferTypes.GraphTransfer; paymentId: Bytes32; - responseCID: Bytes32; - signature: SignatureString; + responseCID?: Bytes32; + signature?: SignatureString; }; type ResolveGraphSignedTransferResponse = { From bfee042180afb9ce9173985f8177aefc6b511690 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 21:47:45 -0700 Subject: [PATCH 15/36] reset --- modules/contracts/address-book.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/contracts/address-book.json b/modules/contracts/address-book.json index 92368f5b8c..dfb084c426 100644 --- a/modules/contracts/address-book.json +++ b/modules/contracts/address-book.json @@ -635,10 +635,10 @@ "txHash": "0x5280bbe977786401736d4156c559eb1e84bfb44b14fc114822873a8bde5bcdde" }, "HashLockTransferApp": { - "address": "0x8f0483125FCb9aaAEFA9209D8E9d7b9C8B9Fb90F", - "creationCodeHash": "0x4978fb7ee1a6ff60d510d9f7242898d1040ed8af0414d3ebbb4cd4696e4a84b4", - "runtimeCodeHash": "0xfd55d382a4df61fbd807b7480abc0c202eb9840aea6c00e7e6f9073b08e0b193", - "txHash": "0x556cefd1241cc8a0597eb01b901e60c2736a0812f0b2faeeffc3415daf79a3a4" + "address": "0x2d63Bdb2efFC4C8f5bAEC9B4fE87e18E9818D384", + "creationCodeHash": "0x3c7dca6d6adb002d5fe9430ee5f5b6d1150a3dc2686aedde8e899ea59848b1e5", + "runtimeCodeHash": "0xc9abe7901965be973f7c944578049a100b7726859dd0ac87f96ee3ca397f89e6", + "txHash": "0xffd5fd631d6103792b751a70bcc3595c1702c227450d6f53371d46907e2b8491" }, "IdentityApp": { "address": "0x9FBDa871d559710256a2502A2517b794B482Db40", From 12cb1f263b99e8cfffb739d60218cf752f4f3d43 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 21:47:53 -0700 Subject: [PATCH 16/36] tests created --- .../src/transfer/hashLockTransfer.test.ts | 135 ++++++++++++++++-- 1 file changed, 124 insertions(+), 11 deletions(-) diff --git a/modules/test-runner/src/transfer/hashLockTransfer.test.ts b/modules/test-runner/src/transfer/hashLockTransfer.test.ts index 23919bf52d..d0052f38fd 100644 --- a/modules/test-runner/src/transfer/hashLockTransfer.test.ts +++ b/modules/test-runner/src/transfer/hashLockTransfer.test.ts @@ -19,7 +19,6 @@ import { fundChannel, TOKEN_AMOUNT, env, - requestCollateral, } from "../util"; const { AddressZero, HashZero } = constants; @@ -117,12 +116,15 @@ describe.only("HashLock Transfers", () => { transfer: AssetOptions & { preImage: string; timelock: string }, waitForSender: boolean = true, ) => { + const lockHash = soliditySha256(["bytes32"], [transfer.preImage]); + const paymentId = soliditySha256(["address", "bytes32"], [transfer.assetId, lockHash]); return Promise.all([ // receiver result receiver.resolveCondition({ conditionType: ConditionalTransferTypes.HashLockTransfer, preImage: transfer.preImage, assetId: transfer.assetId, + paymentId, }), // receiver event new Promise((resolve, reject) => { @@ -169,12 +171,12 @@ describe.only("HashLock Transfers", () => { preImage: string; senderIdentifier: string; receiverIdentifier: string; - paymentId: string; }, expected: Partial = {}, ) => { const lockHash = soliditySha256(["bytes32"], [transfer.preImage]); const retrieved = await client.getHashLockTransfer(lockHash, transfer.assetId); + const paymentId = soliditySha256(["address", "bytes32"], [transfer.assetId, lockHash]); expect(retrieved).to.containSubset({ amount: transfer.amount.toString(), assetId: transfer.assetId, @@ -184,7 +186,7 @@ describe.only("HashLock Transfers", () => { meta: { sender: transfer.senderIdentifier, timelock: transfer.timelock, - paymentId: transfer.paymentId, + paymentId, }, ...expected, }); @@ -237,7 +239,7 @@ describe.only("HashLock Transfers", () => { const timelock = (5000).toString(); const opts = { ...transfer, preImage, timelock }; - const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); + await sendHashlockTransfer(clientA, clientB, opts); await assertRetrievedTransfer( clientB, @@ -245,7 +247,6 @@ describe.only("HashLock Transfers", () => { ...opts, senderIdentifier: clientA.publicIdentifier, receiverIdentifier: clientB.publicIdentifier, - paymentId: paymentId!, }, { status: HashLockTransferStatus.PENDING, @@ -260,7 +261,7 @@ describe.only("HashLock Transfers", () => { const timelock = (5000).toString(); const opts = { ...transfer, preImage, timelock }; - const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); + await sendHashlockTransfer(clientA, clientB, opts); // wait for transfer to be picked up by receiver, but not reclaim // by node for sender @@ -272,7 +273,6 @@ describe.only("HashLock Transfers", () => { ...opts, senderIdentifier: clientA.publicIdentifier, receiverIdentifier: clientB.publicIdentifier, - paymentId: paymentId!, }, { status: HashLockTransferStatus.COMPLETED, @@ -306,13 +306,14 @@ describe.only("HashLock Transfers", () => { const timelock = (5000).toString(); const opts = { ...transfer, preImage, timelock }; - await sendHashlockTransfer(clientA, clientB, opts); + const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); const badPreImage = getRandomBytes32(); await expect( clientB.resolveCondition({ conditionType: ConditionalTransferTypes.HashLockTransfer, preImage: badPreImage, + paymentId: paymentId!, assetId: transfer.assetId, } as PublicParams.ResolveHashLockTransfer), ).to.eventually.be.rejectedWith(/app has not been installed/); @@ -327,7 +328,7 @@ describe.only("HashLock Transfers", () => { const timelock = (101).toString(); const opts = { ...transfer, preImage, timelock }; - await sendHashlockTransfer(clientA, clientB, opts); + const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); await new Promise((resolve) => provider.once("block", resolve)); @@ -335,6 +336,7 @@ describe.only("HashLock Transfers", () => { clientB.resolveCondition({ conditionType: ConditionalTransferTypes.HashLockTransfer, preImage, + paymentId: paymentId!, assetId: transfer.assetId, } as PublicParams.ResolveHashLockTransfer), ).to.be.rejectedWith(/Cannot take action if expiry is expired/); @@ -374,7 +376,118 @@ describe.only("HashLock Transfers", () => { ).to.be.fulfilled; }); - it("should be able to cancel an active payment", async () => {}); + it.only("receiver should be able to cancel an active payment", async () => { + const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; + const preImage = getRandomBytes32(); + const timelock = (5000).toString(); + const opts = { ...transfer, preImage, timelock }; + + const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); + await assertSenderDecrement(clientA, opts); + console.log("sender bal properly decremented"); + const { [clientB.signerAddress]: initialBal } = await clientB.getFreeBalance(transfer.assetId); + expect(initialBal).to.eq(0); + console.log("receiver has 0 starting balance"); + + await new Promise((resolve, reject) => { + clientA.once(EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, resolve); + clientB + .resolveCondition({ + paymentId: paymentId!, + conditionType: ConditionalTransferTypes.HashLockTransfer, + assetId: transfer.assetId, + }) + .catch((e) => reject(e)); + }); + + const { [clientA.signerAddress]: senderBal } = await clientA.getFreeBalance(transfer.assetId); + const { [clientB.signerAddress]: receiverBal } = await clientB.getFreeBalance(transfer.assetId); + expect(senderBal).to.eq(transfer.amount); + console.log("sender bal reverted"); + expect(receiverBal).to.eq(0); + console.log("receiver bal reverted"); + }); + + it.only("receiver should be able to refund an expired payment", async () => { + const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; + const preImage = getRandomBytes32(); + const timelock = (101).toString(); + const opts = { ...transfer, preImage, timelock }; + + const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); + await assertSenderDecrement(clientA, opts); + + await new Promise((resolve) => provider.on("block", resolve)); + + await new Promise((resolve, reject) => { + clientA.once(EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, resolve); + clientB + .resolveCondition({ + paymentId: paymentId!, + conditionType: ConditionalTransferTypes.HashLockTransfer, + assetId: transfer.assetId, + }) + .catch((e) => reject(e)); + }); + + const { [clientA.signerAddress]: senderBal } = await clientA.getFreeBalance(transfer.assetId); + const { [clientB.signerAddress]: receiverBal } = await clientB.getFreeBalance(transfer.assetId); + expect(senderBal).to.eq(transfer.amount); + expect(receiverBal).to.eq(0); + }); + + it.skip("sender should be able to refund an expired payment", async () => { + const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; + const preImage = getRandomBytes32(); + const timelock = (1).toString(); + const opts = { ...transfer, preImage, timelock }; + + const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); + + await assertSenderDecrement(clientA, opts); - it("should be able to refund an expired payment", async () => {}); + // FIXME: how to move blocks successfully? + // for (const i of Array(parseInt(timelock) + 5)) { + // await new Promise((resolve) => provider.once("block", resolve)); + // } + + await assertRetrievedTransfer( + clientB, + { + ...opts, + senderIdentifier: clientA.publicIdentifier, + receiverIdentifier: clientB.publicIdentifier, + }, + { + status: HashLockTransferStatus.EXPIRED, + preImage: HashZero, + }, + ); + + await clientA.resolveCondition({ + paymentId: paymentId!, + conditionType: ConditionalTransferTypes.HashLockTransfer, + assetId: transfer.assetId, + }); + + // make sure payment was reverted in balances + const { [clientA.signerAddress]: senderBal } = await clientA.getFreeBalance(transfer.assetId); + const { [clientB.signerAddress]: receiverBal } = await clientB.getFreeBalance(transfer.assetId); + expect(senderBal).to.eq(transfer.amount); + expect(receiverBal).to.eq(0); + + // make sure payment says failed on node + await assertRetrievedTransfer( + clientB, + { + ...opts, + senderIdentifier: clientA.publicIdentifier, + receiverIdentifier: clientB.publicIdentifier, + }, + { + status: HashLockTransferStatus.FAILED, + preImage: HashZero, + }, + ); + }); }); From 4759dddaa9ce8425fd2428140369f83b8347672b Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Fri, 10 Jul 2020 21:48:00 -0700 Subject: [PATCH 17/36] node side --- .../src/appRegistry/appActions.service.ts | 2 +- .../src/appRegistry/appRegistry.service.ts | 85 ++++++++++++++++++- modules/node/src/listener/listener.service.ts | 13 ++- modules/node/src/utils.ts | 7 +- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/modules/node/src/appRegistry/appActions.service.ts b/modules/node/src/appRegistry/appActions.service.ts index 4345e7dbf7..8d23e0e857 100644 --- a/modules/node/src/appRegistry/appActions.service.ts +++ b/modules/node/src/appRegistry/appActions.service.ts @@ -27,7 +27,7 @@ export class AppActionsService { private readonly transferService: TransferService, private readonly withdrawRepository: WithdrawRepository, ) { - this.log.setContext("AppRegistryService"); + this.log.setContext("AppActionsService"); } async handleAppAction( diff --git a/modules/node/src/appRegistry/appRegistry.service.ts b/modules/node/src/appRegistry/appRegistry.service.ts index 74cbb266f6..627944d441 100644 --- a/modules/node/src/appRegistry/appRegistry.service.ts +++ b/modules/node/src/appRegistry/appRegistry.service.ts @@ -21,8 +21,14 @@ import { getTransferTypeFromAppName, DefaultApp, SupportedApplicationNames, + AppState, + AppAction, + HashLockTransferAppName, + GraphSignedTransferAppName, + SimpleSignedTransferAppName, + SimpleLinkedTransferAppName, } from "@connext/types"; -import { getAddressFromAssetId } from "@connext/utils"; +import { getAddressFromAssetId, safeJsonStringify, toBN, getRandomBytes32 } from "@connext/utils"; import { Injectable, OnModuleInit } from "@nestjs/common"; import { BigNumber, providers } from "ethers"; @@ -37,6 +43,7 @@ import { LoggerService } from "../logger/logger.service"; import { SwapRateService } from "../swapRate/swapRate.service"; import { WithdrawService } from "../withdraw/withdraw.service"; import { TransferService } from "../transfer/transfer.service"; +import { GraphSignedTransferApp } from "@connext/contracts"; @Injectable() export class AppRegistryService implements OnModuleInit { @@ -180,6 +187,82 @@ export class AppRegistryService implements OnModuleInit { ); } + public async runPostUninstallTasks( + appName: SupportedApplicationNames, + app: AppInstanceJson, + latestState: AppState, + ): Promise { + this.log.info( + `runPostUninstallTasks for ${appName} ${app.identityHash}, latest state ${safeJsonStringify( + latestState, + )} started`, + ); + // if the uninstalled app was a cancelled transfer app or + // payment, uninstall corresponding sender/receiver payment + if (!Object.keys(ConditionalTransferAppNames).includes(appName)) { + this.log.info(`handleAppAction for app name ${appName} ${app.identityHash} complete`); + return; + } + + const state = latestState as GenericConditionalTransferAppState; + const receiverAppUninstalled = + state.coinTransfers[0].to === (await this.configService.getSignerAddress()); + + if (toBN(state.coinTransfers[0].amount).isZero()) { + // payment is not being cancelled, nothing to handle on uninstall + this.log.info(`Payment was uninstalled but not cancelled, doing nothing.`); + this.log.info(`handleAppAction for app name ${appName} ${app.identityHash} complete`); + return; + } + + this.log.info(`Payment uninstalled without balance change, proceeding with cancellation`); + + // get the sender app for the payment + const secondLeg = receiverAppUninstalled + ? await this.transferService.findSenderAppByPaymentId(app.meta.paymentId) + : await this.transferService.findReceiverAppByPaymentId(app.meta.paymentId); + if (!secondLeg || secondLeg.type !== AppType.INSTANCE) { + this.log.warn(`No installed app found for second leg of payment`); + this.log.info(`handleAppAction for app name ${appName} ${app.identityHash} complete`); + return; + } + + // Proceed with cancelling second leg of payment + let action = undefined; + switch (appName as ConditionalTransferAppNames) { + case HashLockTransferAppName: { + // if the app isnt expired, and node is receiver, uninstall with invalid + // action + const current = await this.configService.getEthProvider().getBlockNumber(); + if ( + toBN((state as HashLockTransferAppState).expiry).gt(current) && + receiverAppUninstalled + ) { + action = { preImage: getRandomBytes32() }; + } + break; + } + case GraphSignedTransferAppName: + case SimpleSignedTransferAppName: + case SimpleLinkedTransferAppName: { + // No expiry, just uninstall without action + break; + } + default: { + throw new Error( + `Unable to cancel payment, unsupported conditional transfer app ${appName}`, + ); + } + } + + // uninstall app without any action + await this.cfCoreService.uninstallApp( + secondLeg.identityHash, + secondLeg.channel.multisigAddress, + action, + ); + } + // APP SPECIFIC MIDDLEWARE public generateMiddleware = async (): Promise< (protocol: ProtocolName, cxt: MiddlewareContext) => Promise diff --git a/modules/node/src/listener/listener.service.ts b/modules/node/src/listener/listener.service.ts index e78840b1bc..e89f368b44 100644 --- a/modules/node/src/listener/listener.service.ts +++ b/modules/node/src/listener/listener.service.ts @@ -185,10 +185,10 @@ export default class ListenerService implements OnModuleInit { return; } const channel = await this.channelRepository.findByMultisigAddressOrThrow(multisigAddress); + const appRegistryInfo = this.cfCoreService.getAppInfoByAppDefinitionAddress( + uninstalledApp.appDefinition, + ); if (action) { - const appRegistryInfo = this.cfCoreService.getAppInfoByAppDefinitionAddress( - uninstalledApp.appDefinition, - ); await this.appActionsService.handleAppAction( appRegistryInfo.name as SupportedApplicationNames, uninstalledApp, @@ -196,6 +196,13 @@ export default class ListenerService implements OnModuleInit { action as AppAction, ); } + + await this.appRegistryService.runPostUninstallTasks( + appRegistryInfo.name as SupportedApplicationNames, + uninstalledApp, + uninstalledApp.latestState as any, // AppState (excluding simple swap app) + ); + const assetIdResponder = ( await this.appInstanceRepository.findByIdentityHashOrThrow(data.data.appIdentityHash) ).responderDepositAssetId; diff --git a/modules/node/src/utils.ts b/modules/node/src/utils.ts index bb8e14878e..d62fc4ae96 100644 --- a/modules/node/src/utils.ts +++ b/modules/node/src/utils.ts @@ -7,7 +7,7 @@ import { HashLockTransferAppState, GenericConditionalTransferAppState, } from "@connext/types"; -import { bigNumberifyJson, toBN } from "@connext/utils"; +import { bigNumberifyJson, toBN, stringify } from "@connext/utils"; import { BigNumber, constants } from "ethers"; import { AppInstance, AppType } from "./appInstance/appInstance.entity"; @@ -93,7 +93,6 @@ export function appStatusesToTransferWithExpiryStatus( return undefined; } const statusWithoutExpiry = appStatusesToTransferStatus(senderApp, receiverApp); - // TODO: will transfer statuses always trump expiries? if (statusWithoutExpiry !== TransferStatuses.PENDING) { return statusWithoutExpiry; } @@ -101,8 +100,8 @@ export function appStatusesToTransferWithExpiryStatus( receiverApp?.latestState || {}, ) as HashLockTransferAppState; const senderState = bigNumberifyJson(senderApp.latestState) as HashLockTransferAppState; - const isSenderExpired = senderState.expiry && senderState.expiry.lt(currentBlockNumber); - const isReceiverExpired = receiverState.expiry && receiverState.expiry.lt(currentBlockNumber); + const isSenderExpired = senderState.expiry && senderState.expiry.lte(currentBlockNumber); + const isReceiverExpired = receiverState.expiry && receiverState.expiry.lte(currentBlockNumber); return isSenderExpired || isReceiverExpired ? TransferWithExpiryStatuses.EXPIRED : TransferWithExpiryStatuses.PENDING; From 41a3c29b96adccb61fcbe34292bfa9fcb8ced563 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 10:55:49 -0700 Subject: [PATCH 18/36] use expiry --- .../controllers/ResolveTransferController.ts | 71 +++++-------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/modules/client/src/controllers/ResolveTransferController.ts b/modules/client/src/controllers/ResolveTransferController.ts index fdf3e01f99..89102af92f 100644 --- a/modules/client/src/controllers/ResolveTransferController.ts +++ b/modules/client/src/controllers/ResolveTransferController.ts @@ -10,13 +10,13 @@ import { SimpleLinkedTransferAppAction, GraphSignedTransferAppAction, AppInstanceJson, - HashLockTransferAppState, } from "@connext/types"; -import { stringify, getRandomBytes32, toBN } from "@connext/utils"; -import { BigNumber } from "ethers"; +import { stringify, toBN } from "@connext/utils"; +import { BigNumber, constants } from "ethers"; import { AbstractController } from "./AbstractController"; +const { HashZero } = constants; export class ResolveTransferController extends AbstractController { public resolveTransfer = async ( params: PublicParams.ResolveCondition, @@ -286,57 +286,20 @@ export class ResolveTransferController extends AbstractController { sender: paymentApp.meta.sender, }; - switch (conditionType) { - case ConditionalTransferTypes.HashLockTransfer: { - // if it is the sender app, can only cancel if the app has expired - const state = paymentApp.latestState as HashLockTransferAppState; - const isSender = state.coinTransfers[0].to === this.connext.signerAddress; - - if (isSender) { - // uninstall app - this.log.info( - `[${paymentId}] Uninstalling transfer app without action ${paymentApp.identityHash}`, - ); - await this.connext.uninstallApp(paymentApp.identityHash); - this.log.info( - `[${paymentId}] Finished uninstalling transfer app ${paymentApp.identityHash}`, - ); - return ret; - } - - let action = undefined; - if (toBN(await this.ethProvider.getBlockNumber()).lt(toBN(state.expiry))) { - // uninstall with bad action iff the app is active, otherwise just - // uninstall - action = { preImage: getRandomBytes32() }; - } - this.log.info( - `[${paymentId}] Uninstalling transfer app with empty action ${paymentApp.identityHash}`, - ); - console.log(`uninstalling payment with action`, action); - await this.connext.uninstallApp(paymentApp.identityHash, action); - this.log.info( - `[${paymentId}] Finished uninstalling transfer app ${paymentApp.identityHash}`, - ); - return ret; - } - case ConditionalTransferTypes.GraphTransfer: - case ConditionalTransferTypes.SignedTransfer: - case ConditionalTransferTypes.LinkedTransfer: { - // uninstall the app without taking action - this.log.info( - `[${paymentId}] Uninstalling transfer app without action ${paymentApp.identityHash}`, - ); - await this.connext.uninstallApp(paymentApp.identityHash); - this.log.info( - `[${paymentId}] Finished uninstalling transfer app ${paymentApp.identityHash}`, - ); - return ret; - } - default: { - const c: never = conditionType; - throw new Error(`Unable to cancel payment, unsupported condition ${c}`); - } + let action: HashLockTransferAppAction | undefined = undefined; + // IFF payment app has an expiry, the app has not expired, and the user + // is the receiver of the payment, play an invalid action + const state = paymentApp.latestState as any; + const block = await this.ethProvider.getBlockNumber(); + if ( + state.coinTransfers[0].to === this.connext.signerAddress && + toBN(state.expiry || 0).gt(block) + ) { + action = { preImage: HashZero }; } + this.log.info(`[${paymentId}] Uninstalling transfer app with action ${stringify(action)}`); + await this.connext.uninstallApp(paymentApp.identityHash, action); + this.log.info(`[${paymentId}] Uninstalled transfer app ${paymentApp.identityHash}`); + return ret; } } From ecd4e08530aa81f3dccfdbdad2eb5c7f05714cba Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 10:56:07 -0700 Subject: [PATCH 19/36] types --- modules/node/src/appRegistry/appActions.service.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/node/src/appRegistry/appActions.service.ts b/modules/node/src/appRegistry/appActions.service.ts index 8d23e0e857..1bb8efb227 100644 --- a/modules/node/src/appRegistry/appActions.service.ts +++ b/modules/node/src/appRegistry/appActions.service.ts @@ -95,10 +95,7 @@ export class AppActionsService { await this.withdrawService.submitWithdrawToChain(appInstance.multisigAddress, tx); } - private async handleTransferAppAction( - senderApp: AppInstance, - action: AppAction, - ): Promise { + private async handleTransferAppAction(senderApp: AppInstance, action: AppAction): Promise { // App could be uninstalled, which means the channel is no longer // associated with this app instance if (senderApp.type !== AppType.INSTANCE) { From 5bb436cb2e89f8c91974a76bcdf5d89c4be250e6 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 10:56:58 -0700 Subject: [PATCH 20/36] handle cancellation --- .../src/appRegistry/appRegistry.service.ts | 65 +++++++------------ modules/node/src/listener/listener.service.ts | 12 ++-- 2 files changed, 28 insertions(+), 49 deletions(-) diff --git a/modules/node/src/appRegistry/appRegistry.service.ts b/modules/node/src/appRegistry/appRegistry.service.ts index 627944d441..4d6b3f7d50 100644 --- a/modules/node/src/appRegistry/appRegistry.service.ts +++ b/modules/node/src/appRegistry/appRegistry.service.ts @@ -187,20 +187,22 @@ export class AppRegistryService implements OnModuleInit { ); } - public async runPostUninstallTasks( + // should handle: + // - cancellation cases of conditional payments + public async handleAppUninstall( appName: SupportedApplicationNames, app: AppInstanceJson, latestState: AppState, ): Promise { this.log.info( - `runPostUninstallTasks for ${appName} ${app.identityHash}, latest state ${safeJsonStringify( + `handleAppUninstall for ${appName} ${app.identityHash}, latest state ${safeJsonStringify( latestState, )} started`, ); // if the uninstalled app was a cancelled transfer app or // payment, uninstall corresponding sender/receiver payment if (!Object.keys(ConditionalTransferAppNames).includes(appName)) { - this.log.info(`handleAppAction for app name ${appName} ${app.identityHash} complete`); + this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); return; } @@ -211,56 +213,29 @@ export class AppRegistryService implements OnModuleInit { if (toBN(state.coinTransfers[0].amount).isZero()) { // payment is not being cancelled, nothing to handle on uninstall this.log.info(`Payment was uninstalled but not cancelled, doing nothing.`); - this.log.info(`handleAppAction for app name ${appName} ${app.identityHash} complete`); + this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); return; } - this.log.info(`Payment uninstalled without balance change, proceeding with cancellation`); - - // get the sender app for the payment + // get the second leg for the payment (either party can cancel payment) const secondLeg = receiverAppUninstalled ? await this.transferService.findSenderAppByPaymentId(app.meta.paymentId) : await this.transferService.findReceiverAppByPaymentId(app.meta.paymentId); if (!secondLeg || secondLeg.type !== AppType.INSTANCE) { this.log.warn(`No installed app found for second leg of payment`); - this.log.info(`handleAppAction for app name ${appName} ${app.identityHash} complete`); + this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); return; } // Proceed with cancelling second leg of payment - let action = undefined; - switch (appName as ConditionalTransferAppNames) { - case HashLockTransferAppName: { - // if the app isnt expired, and node is receiver, uninstall with invalid - // action - const current = await this.configService.getEthProvider().getBlockNumber(); - if ( - toBN((state as HashLockTransferAppState).expiry).gt(current) && - receiverAppUninstalled - ) { - action = { preImage: getRandomBytes32() }; - } - break; - } - case GraphSignedTransferAppName: - case SimpleSignedTransferAppName: - case SimpleLinkedTransferAppName: { - // No expiry, just uninstall without action - break; - } - default: { - throw new Error( - `Unable to cancel payment, unsupported conditional transfer app ${appName}`, - ); - } - } - - // uninstall app without any action + this.log.info( + `Payment uninstalled without balance change uninstalling second leg of payment ${secondLeg.identityHash}`, + ); await this.cfCoreService.uninstallApp( secondLeg.identityHash, secondLeg.channel.multisigAddress, - action, ); + this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); } // APP SPECIFIC MIDDLEWARE @@ -451,14 +426,18 @@ export class AppRegistryService implements OnModuleInit { ); } - // double check that the app was uninstalled - if (receiverApp.type !== AppType.UNINSTALLED) { - throw new Error(`Receiver app was unable to be uninstalled`); + // double check that receiver app state has not been finalized + // only allow sender uninstall prior to receiver uninstall IFF the hub + // has not paid receiver. Receiver app will be uninstalled again on event + if ( + senderAppLatestState.coinTransfers[1].amount.isZero() && // not reclaimed + receiverApp.latestState.coinTransfers[0].amount.isZero() // finalized + ) { + throw new Error( + `Cannot uninstall unfinalized sender app, receiver app has payment has been completed`, + ); } - if (!senderAppLatestState.finalized && receiverApp.latestState.finalized) { - throw new Error(`Cannot uninstall unfinalized sender app, receiver app has been finalized`); - } this.log.info(`Finished uninstallTransferMiddleware for ${appInstance.identityHash}`); }; diff --git a/modules/node/src/listener/listener.service.ts b/modules/node/src/listener/listener.service.ts index e89f368b44..272b6005b9 100644 --- a/modules/node/src/listener/listener.service.ts +++ b/modules/node/src/listener/listener.service.ts @@ -195,14 +195,14 @@ export default class ListenerService implements OnModuleInit { uninstalledApp.latestState as any, // AppState (excluding simple swap app) action as AppAction, ); + } else { + await this.appRegistryService.handleAppUninstall( + appRegistryInfo.name as SupportedApplicationNames, + uninstalledApp, + uninstalledApp.latestState as any, // AppState (excluding simple swap app) + ); } - await this.appRegistryService.runPostUninstallTasks( - appRegistryInfo.name as SupportedApplicationNames, - uninstalledApp, - uninstalledApp.latestState as any, // AppState (excluding simple swap app) - ); - const assetIdResponder = ( await this.appInstanceRepository.findByIdentityHashOrThrow(data.data.appIdentityHash) ).responderDepositAssetId; From 4f798cad9e12f1f75393b8df75bd64acd13698fc Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 11:32:32 -0700 Subject: [PATCH 21/36] handle proposal case better --- .../src/appRegistry/appRegistry.service.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/node/src/appRegistry/appRegistry.service.ts b/modules/node/src/appRegistry/appRegistry.service.ts index 4d6b3f7d50..821d30cbd8 100644 --- a/modules/node/src/appRegistry/appRegistry.service.ts +++ b/modules/node/src/appRegistry/appRegistry.service.ts @@ -221,9 +221,24 @@ export class AppRegistryService implements OnModuleInit { const secondLeg = receiverAppUninstalled ? await this.transferService.findSenderAppByPaymentId(app.meta.paymentId) : await this.transferService.findReceiverAppByPaymentId(app.meta.paymentId); - if (!secondLeg || secondLeg.type !== AppType.INSTANCE) { - this.log.warn(`No installed app found for second leg of payment`); - this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); + if (!secondLeg) { + return; + } + + // check case where sender app is cancelled, receiver never had proposal + if (secondLeg.type !== AppType.INSTANCE) { + if (!receiverAppUninstalled && secondLeg.type === AppType.PROPOSAL) { + this.log.info(`Sender cancelled payment, rejecting receiver proposal`); + await this.cfCoreService.rejectInstallApp( + secondLeg.identityHash, + secondLeg.channel.multisigAddress, + "Sender cancelled payment", + ); + this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); + return; + } + // otherwise no second leg installed, return + this.log.info(`No second leg found for cancelled transfer, doing nothing`); return; } From 587577ad0e726874d33d946dd65c571a4f7641ad Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 11:38:45 -0700 Subject: [PATCH 22/36] handle case where receiver app expired, sender did not --- .../src/appRegistry/appRegistry.service.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/node/src/appRegistry/appRegistry.service.ts b/modules/node/src/appRegistry/appRegistry.service.ts index 821d30cbd8..ada22a69c4 100644 --- a/modules/node/src/appRegistry/appRegistry.service.ts +++ b/modules/node/src/appRegistry/appRegistry.service.ts @@ -22,15 +22,11 @@ import { DefaultApp, SupportedApplicationNames, AppState, - AppAction, - HashLockTransferAppName, - GraphSignedTransferAppName, - SimpleSignedTransferAppName, - SimpleLinkedTransferAppName, + HashLockTransferAppAction, } from "@connext/types"; -import { getAddressFromAssetId, safeJsonStringify, toBN, getRandomBytes32 } from "@connext/utils"; +import { getAddressFromAssetId, safeJsonStringify, toBN } from "@connext/utils"; import { Injectable, OnModuleInit } from "@nestjs/common"; -import { BigNumber, providers } from "ethers"; +import { BigNumber, providers, constants } from "ethers"; import { AppType } from "../appInstance/appInstance.entity"; import { CFCoreService } from "../cfCore/cfCore.service"; @@ -43,7 +39,8 @@ import { LoggerService } from "../logger/logger.service"; import { SwapRateService } from "../swapRate/swapRate.service"; import { WithdrawService } from "../withdraw/withdraw.service"; import { TransferService } from "../transfer/transfer.service"; -import { GraphSignedTransferApp } from "@connext/contracts"; + +const { HashZero } = constants; @Injectable() export class AppRegistryService implements OnModuleInit { @@ -246,9 +243,17 @@ export class AppRegistryService implements OnModuleInit { this.log.info( `Payment uninstalled without balance change uninstalling second leg of payment ${secondLeg.identityHash}`, ); + // handle the case where this is an unexpired sendeder app + let action: HashLockTransferAppAction | undefined = undefined; + const block = await this.configService.getEthProvider().getBlockNumber(); + if (secondLeg.latestState.expiry && toBN(secondLeg.latestState.expiry).gt(block)) { + this.log.info(`Second leg of payment not yet expired, uninstalling with invalid action`); + action = { preImage: HashZero }; + } await this.cfCoreService.uninstallApp( secondLeg.identityHash, secondLeg.channel.multisigAddress, + action, ); this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); } From 859ffa3ad16a0753f42847ce6af814a93ae2c12b Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 11:55:14 -0700 Subject: [PATCH 23/36] reset --- modules/contracts/address-book.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/contracts/address-book.json b/modules/contracts/address-book.json index dfb084c426..3188d0084c 100644 --- a/modules/contracts/address-book.json +++ b/modules/contracts/address-book.json @@ -635,10 +635,10 @@ "txHash": "0x5280bbe977786401736d4156c559eb1e84bfb44b14fc114822873a8bde5bcdde" }, "HashLockTransferApp": { - "address": "0x2d63Bdb2efFC4C8f5bAEC9B4fE87e18E9818D384", + "address": "0x8f0483125FCb9aaAEFA9209D8E9d7b9C8B9Fb90F", "creationCodeHash": "0x3c7dca6d6adb002d5fe9430ee5f5b6d1150a3dc2686aedde8e899ea59848b1e5", "runtimeCodeHash": "0xc9abe7901965be973f7c944578049a100b7726859dd0ac87f96ee3ca397f89e6", - "txHash": "0xffd5fd631d6103792b751a70bcc3595c1702c227450d6f53371d46907e2b8491" + "txHash": "0xde79991981bd38286454f95a413a5c4f92128042c90ebc12c7bb78bac1d1dc59" }, "IdentityApp": { "address": "0x9FBDa871d559710256a2502A2517b794B482Db40", From 8dc030fa3a356b921b9b455b1dfa980b273a709f Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 11:55:23 -0700 Subject: [PATCH 24/36] fix index and logs --- modules/client/src/controllers/ResolveTransferController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/client/src/controllers/ResolveTransferController.ts b/modules/client/src/controllers/ResolveTransferController.ts index 89102af92f..a45c753e4b 100644 --- a/modules/client/src/controllers/ResolveTransferController.ts +++ b/modules/client/src/controllers/ResolveTransferController.ts @@ -53,7 +53,7 @@ export class ResolveTransferController extends AbstractController { if (!this.hasSecret(params)) { // User is cancelling the payment try { - console.log(`trying to cancel payment`); + this.log.info(`[${paymentId}] Cancelling payment`); const ret = await this.handleCancellation(params); this.log.info(`[${paymentId}] resolveCondition complete: ${stringify(ret)}`); return ret; @@ -292,7 +292,7 @@ export class ResolveTransferController extends AbstractController { const state = paymentApp.latestState as any; const block = await this.ethProvider.getBlockNumber(); if ( - state.coinTransfers[0].to === this.connext.signerAddress && + state.coinTransfers[1].to === this.connext.signerAddress && toBN(state.expiry || 0).gt(block) ) { action = { preImage: HashZero }; From c4f709306231e6e3f40eda232195327519a64ac8 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 11:55:54 -0700 Subject: [PATCH 25/36] tests passing --- modules/test-runner/src/transfer/hashLockTransfer.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/test-runner/src/transfer/hashLockTransfer.test.ts b/modules/test-runner/src/transfer/hashLockTransfer.test.ts index d0052f38fd..60572fd821 100644 --- a/modules/test-runner/src/transfer/hashLockTransfer.test.ts +++ b/modules/test-runner/src/transfer/hashLockTransfer.test.ts @@ -376,7 +376,7 @@ describe.only("HashLock Transfers", () => { ).to.be.fulfilled; }); - it.only("receiver should be able to cancel an active payment", async () => { + it("receiver should be able to cancel an active payment", async () => { const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; const preImage = getRandomBytes32(); const timelock = (5000).toString(); @@ -384,10 +384,8 @@ describe.only("HashLock Transfers", () => { const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); await assertSenderDecrement(clientA, opts); - console.log("sender bal properly decremented"); const { [clientB.signerAddress]: initialBal } = await clientB.getFreeBalance(transfer.assetId); expect(initialBal).to.eq(0); - console.log("receiver has 0 starting balance"); await new Promise((resolve, reject) => { clientA.once(EventNames.CONDITIONAL_TRANSFER_UNLOCKED_EVENT, resolve); @@ -403,12 +401,10 @@ describe.only("HashLock Transfers", () => { const { [clientA.signerAddress]: senderBal } = await clientA.getFreeBalance(transfer.assetId); const { [clientB.signerAddress]: receiverBal } = await clientB.getFreeBalance(transfer.assetId); expect(senderBal).to.eq(transfer.amount); - console.log("sender bal reverted"); expect(receiverBal).to.eq(0); - console.log("receiver bal reverted"); }); - it.only("receiver should be able to refund an expired payment", async () => { + it("receiver should be able to cancel an expired payment", async () => { const transfer: AssetOptions = { amount: TOKEN_AMOUNT, assetId: tokenAddress }; const preImage = getRandomBytes32(); const timelock = (101).toString(); From bd5bc697b29c3f60cdae11615bb62ef65aa9c81b Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 11:59:51 -0700 Subject: [PATCH 26/36] remove .only, fix test --- .../src/transfer/hashLockTransfer.test.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/modules/test-runner/src/transfer/hashLockTransfer.test.ts b/modules/test-runner/src/transfer/hashLockTransfer.test.ts index 60572fd821..4fca4f041a 100644 --- a/modules/test-runner/src/transfer/hashLockTransfer.test.ts +++ b/modules/test-runner/src/transfer/hashLockTransfer.test.ts @@ -26,7 +26,7 @@ const { soliditySha256 } = utils; const TIMEOUT_BUFFER = 100; // This currently isn't exported by the node so must be hardcoded -describe.only("HashLock Transfers", () => { +describe("HashLock Transfers", () => { let clientA: IConnextClient; let clientB: IConnextClient; let tokenAddress: string; @@ -309,14 +309,16 @@ describe.only("HashLock Transfers", () => { const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); const badPreImage = getRandomBytes32(); - await expect( - clientB.resolveCondition({ - conditionType: ConditionalTransferTypes.HashLockTransfer, - preImage: badPreImage, - paymentId: paymentId!, - assetId: transfer.assetId, - } as PublicParams.ResolveHashLockTransfer), - ).to.eventually.be.rejectedWith(/app has not been installed/); + await clientB.resolveCondition({ + conditionType: ConditionalTransferTypes.HashLockTransfer, + preImage: badPreImage, + paymentId: paymentId!, + assetId: transfer.assetId, + } as PublicParams.ResolveHashLockTransfer); + + // verfy payment did not go through + const { [clientB.signerAddress]: receiverBal } = await clientB.getFreeBalance(transfer.assetId); + expect(receiverBal).to.eq(0); }); // NOTE: if the node tries to collateralize or send a transaction during From 44d5f13bb42a7b08eb3d48bb4f999ac6a2079129 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Sat, 11 Jul 2020 12:17:46 -0700 Subject: [PATCH 27/36] fix casting --- modules/node/src/appRegistry/appRegistry.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/node/src/appRegistry/appRegistry.service.ts b/modules/node/src/appRegistry/appRegistry.service.ts index ada22a69c4..492cad6042 100644 --- a/modules/node/src/appRegistry/appRegistry.service.ts +++ b/modules/node/src/appRegistry/appRegistry.service.ts @@ -450,8 +450,8 @@ export class AppRegistryService implements OnModuleInit { // only allow sender uninstall prior to receiver uninstall IFF the hub // has not paid receiver. Receiver app will be uninstalled again on event if ( - senderAppLatestState.coinTransfers[1].amount.isZero() && // not reclaimed - receiverApp.latestState.coinTransfers[0].amount.isZero() // finalized + toBN(senderAppLatestState.coinTransfers[1].amount).isZero() && // not reclaimed + toBN(receiverApp.latestState.coinTransfers[0].amount).isZero() // finalized ) { throw new Error( `Cannot uninstall unfinalized sender app, receiver app has payment has been completed`, From 25e4237b6a7ba2008275cb72638a21954149e5b1 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 16:17:43 -0700 Subject: [PATCH 28/36] prettier --- .../src.sol/apps/GraphSignedTransferApp.sol | 229 +++++++++--------- .../src.sol/apps/SimpleLinkedTransferApp.sol | 112 ++++----- .../src.sol/apps/SimpleSignedTransferApp.sol | 211 ++++++++-------- 3 files changed, 259 insertions(+), 293 deletions(-) diff --git a/modules/contracts/src.sol/apps/GraphSignedTransferApp.sol b/modules/contracts/src.sol/apps/GraphSignedTransferApp.sol index e77b94e590..f29bd7bd1c 100644 --- a/modules/contracts/src.sol/apps/GraphSignedTransferApp.sol +++ b/modules/contracts/src.sol/apps/GraphSignedTransferApp.sol @@ -7,129 +7,122 @@ import "@openzeppelin/contracts/cryptography/ECDSA.sol"; import "../adjudicator/interfaces/CounterfactualApp.sol"; import "../funding/libs/LibOutcome.sol"; - /// @title Simple Signed Transfer App /// @notice This contract allows users to claim a payment locked in /// the application if the specified signed submits the correct /// signature for the provided data contract GraphSignedTransferApp is CounterfactualApp { - using SafeMath for uint256; - - struct AppState { - LibOutcome.CoinTransfer[2] coinTransfers; - address signerAddress; - uint256 chainId; - address verifyingContract; - bytes32 requestCID; - bytes32 subgraphDeploymentID; - bytes32 paymentId; - bool finalized; - } - - struct Action { - bytes32 responseCID; - bytes signature; - } - - // EIP-712 TYPE HASH CONSTANTS - - bytes32 private constant DOMAIN_TYPE_HASH = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" - ); - bytes32 private constant RECEIPT_TYPE_HASH = keccak256( - "Receipt(bytes32 requestCID,bytes32 responseCID,bytes32 subgraphDeploymentID)" - ); - - // EIP-712 DOMAIN SEPARATOR CONSTANTS - - bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Protocol"); - bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); - bytes32 private constant DOMAIN_SALT = 0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2; - - - function recoverAttestationSigner(Action memory action, AppState memory state) public pure returns (address) { - return ECDSA.recover( + using SafeMath for uint256; + + struct AppState { + LibOutcome.CoinTransfer[2] coinTransfers; + address signerAddress; + uint256 chainId; + address verifyingContract; + bytes32 requestCID; + bytes32 subgraphDeploymentID; + bytes32 paymentId; + bool finalized; + } + + struct Action { + bytes32 responseCID; + bytes signature; + } + + // EIP-712 TYPE HASH CONSTANTS + + bytes32 private constant DOMAIN_TYPE_HASH = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ); + bytes32 private constant RECEIPT_TYPE_HASH = keccak256( + "Receipt(bytes32 requestCID,bytes32 responseCID,bytes32 subgraphDeploymentID)" + ); + + // EIP-712 DOMAIN SEPARATOR CONSTANTS + + bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Protocol"); + bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); + bytes32 + private constant DOMAIN_SALT = 0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2; + + function recoverAttestationSigner(Action memory action, AppState memory state) + public + pure + returns (address) + { + return + ECDSA.recover( + keccak256( + abi.encodePacked( + "\x19\x01", keccak256( - abi.encodePacked( - "\x19\x01", - keccak256( - abi.encode( - DOMAIN_TYPE_HASH, - DOMAIN_NAME_HASH, - DOMAIN_VERSION_HASH, - state.chainId, - state.verifyingContract, - DOMAIN_SALT - ) - ), - keccak256( - abi.encode( - RECEIPT_TYPE_HASH, - state.requestCID, - action.responseCID, - state.subgraphDeploymentID - ) - ) - ) + abi.encode( + DOMAIN_TYPE_HASH, + DOMAIN_NAME_HASH, + DOMAIN_VERSION_HASH, + state.chainId, + state.verifyingContract, + DOMAIN_SALT + ) ), - action.signature - ); - } - - - function applyAction( - bytes calldata encodedState, - bytes calldata encodedAction - ) - override - external - view - returns (bytes memory) - { - AppState memory state = abi.decode(encodedState, (AppState)); - Action memory action = abi.decode(encodedAction, (Action)); - - require(!state.finalized, "Cannot take action on finalized state"); - - require(state.signerAddress == recoverAttestationSigner(action, state), "Incorrect signer recovered from signature"); - - state.coinTransfers[1].amount = state.coinTransfers[0].amount; - state.coinTransfers[0].amount = 0; - state.finalized = true; - - return abi.encode(state); - } - - function computeOutcome(bytes calldata encodedState) - override - external - view - returns (bytes memory) - { - AppState memory state = abi.decode(encodedState, (AppState)); - - return abi.encode(state.coinTransfers); - } - - function getTurnTaker( - bytes calldata /* encodedState */, - address[] calldata participants - ) - override - external - view - returns (address) - { - return participants[1]; // receiver should always be indexed at [1] - } - - function isStateTerminal(bytes calldata encodedState) - override - external - view - returns (bool) - { - AppState memory state = abi.decode(encodedState, (AppState)); - return state.finalized; - } + keccak256( + abi.encode( + RECEIPT_TYPE_HASH, + state.requestCID, + action.responseCID, + state.subgraphDeploymentID + ) + ) + ) + ), + action.signature + ); + } + + function applyAction(bytes calldata encodedState, bytes calldata encodedAction) + external + override + view + returns (bytes memory) + { + AppState memory state = abi.decode(encodedState, (AppState)); + Action memory action = abi.decode(encodedAction, (Action)); + + require(!state.finalized, "Cannot take action on finalized state"); + + require( + state.signerAddress == recoverAttestationSigner(action, state), + "Incorrect signer recovered from signature" + ); + + state.coinTransfers[1].amount = state.coinTransfers[0].amount; + state.coinTransfers[0].amount = 0; + state.finalized = true; + + return abi.encode(state); + } + + function computeOutcome(bytes calldata encodedState) + external + override + view + returns (bytes memory) + { + AppState memory state = abi.decode(encodedState, (AppState)); + + return abi.encode(state.coinTransfers); + } + + function getTurnTaker( + bytes calldata, /* encodedState */ + address[] calldata participants + ) external override view returns (address) { + return participants[1]; // receiver should always be indexed at [1] + } + + function isStateTerminal(bytes calldata encodedState) external override view returns (bool) { + AppState memory state = abi.decode(encodedState, (AppState)); + return state.finalized; + } } diff --git a/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol b/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol index 21aba24ab3..68a4582c34 100644 --- a/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol +++ b/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol @@ -6,79 +6,67 @@ import "@openzeppelin/contracts/math/SafeMath.sol"; import "../adjudicator/interfaces/CounterfactualApp.sol"; import "../funding/libs/LibOutcome.sol"; - /// @title Simple Linked Transfer App /// @notice This contract allows users to claim a payment locked in /// the application if they provide the correct preImage contract SimpleLinkedTransferApp is CounterfactualApp { + using SafeMath for uint256; - using SafeMath for uint256; - - struct AppState { - LibOutcome.CoinTransfer[2] coinTransfers; - bytes32 linkedHash; - bytes32 preImage; - bool finalized; - } + struct AppState { + LibOutcome.CoinTransfer[2] coinTransfers; + bytes32 linkedHash; + bytes32 preImage; + bool finalized; + } - struct Action { - bytes32 preImage; - } + struct Action { + bytes32 preImage; + } - function applyAction( - bytes calldata encodedState, - bytes calldata encodedAction - ) - override - external - view - returns (bytes memory) - { - AppState memory state = abi.decode(encodedState, (AppState)); - Action memory action = abi.decode(encodedAction, (Action)); - bytes32 generatedHash = sha256(abi.encode(action.preImage)); + function applyAction(bytes calldata encodedState, bytes calldata encodedAction) + external + override + view + returns (bytes memory) + { + AppState memory state = abi.decode(encodedState, (AppState)); + Action memory action = abi.decode(encodedAction, (Action)); + bytes32 generatedHash = sha256(abi.encode(action.preImage)); - require(!state.finalized, "Cannot take action on finalized state"); - require(state.linkedHash == generatedHash, "Hash generated from preimage does not match hash in state"); + require(!state.finalized, "Cannot take action on finalized state"); + require( + state.linkedHash == generatedHash, + "Hash generated from preimage does not match hash in state" + ); - state.coinTransfers[1].amount = state.coinTransfers[0].amount; - state.coinTransfers[0].amount = 0; - state.preImage = action.preImage; - state.finalized = true; + state.coinTransfers[1].amount = state.coinTransfers[0].amount; + state.coinTransfers[0].amount = 0; + state.preImage = action.preImage; + state.finalized = true; - return abi.encode(state); - } + return abi.encode(state); + } - function computeOutcome(bytes calldata encodedState) - override - external - view - returns (bytes memory) - { - AppState memory state = abi.decode(encodedState, (AppState)); - // Revert payment if it's uninstalled before being finalized - return abi.encode(state.coinTransfers); - } + function computeOutcome(bytes calldata encodedState) + external + override + view + returns (bytes memory) + { + AppState memory state = abi.decode(encodedState, (AppState)); + // Revert payment if it's uninstalled before being finalized + return abi.encode(state.coinTransfers); + } - function getTurnTaker( - bytes calldata /* encodedState */, - address[] calldata participants - ) - override - external - view - returns (address) - { - return participants[1]; // receiver should always be indexed at [1] - } + function getTurnTaker( + bytes calldata, /* encodedState */ + address[] calldata participants + ) external override view returns (address) { + return participants[1]; // receiver should always be indexed at [1] + } - function isStateTerminal(bytes calldata encodedState) - override - external - view - returns (bool) - { - AppState memory state = abi.decode(encodedState, (AppState)); - return state.finalized; - } + function isStateTerminal(bytes calldata encodedState) external override view returns (bool) { + AppState memory state = abi.decode(encodedState, (AppState)); + return state.finalized; + } } diff --git a/modules/contracts/src.sol/apps/SimpleSignedTransferApp.sol b/modules/contracts/src.sol/apps/SimpleSignedTransferApp.sol index 81a6cce52e..a85f4327ef 100644 --- a/modules/contracts/src.sol/apps/SimpleSignedTransferApp.sol +++ b/modules/contracts/src.sol/apps/SimpleSignedTransferApp.sol @@ -7,124 +7,109 @@ import "../adjudicator/interfaces/CounterfactualApp.sol"; import "../funding/libs/LibOutcome.sol"; import "../shared/libs/LibChannelCrypto.sol"; - /// @title Simple Signed Transfer App /// @notice This contract allows users to claim a payment locked in /// the application if the specified signed submits the correct /// signature for the provided data contract SimpleSignedTransferApp is CounterfactualApp { - using SafeMath for uint256; - - struct AppState { - LibOutcome.CoinTransfer[2] coinTransfers; - address signerAddress; - uint256 chainId; - address verifyingContract; - bytes32 domainSeparator; - bytes32 paymentId; - bool finalized; - } - - struct Action { - bytes32 data; - bytes signature; - } - - // EIP-712 DOMAIN SEPARATOR CONSTANTS - bytes32 private constant DOMAIN_TYPE_HASH = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" - ); - bytes32 private constant RECEIPT_TYPE_HASH = keccak256( - "Receipt(bytes32 paymentId,bytes32 data)" - ); - - bytes32 private constant DOMAIN_NAME_HASH = keccak256("Connext Signed Transfer"); - bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); - bytes32 private constant DOMAIN_SALT = 0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2; - - - function recoverSigner(Action memory action, AppState memory state) public pure returns (address) { - return ECDSA.recover( + using SafeMath for uint256; + + struct AppState { + LibOutcome.CoinTransfer[2] coinTransfers; + address signerAddress; + uint256 chainId; + address verifyingContract; + bytes32 domainSeparator; + bytes32 paymentId; + bool finalized; + } + + struct Action { + bytes32 data; + bytes signature; + } + + // EIP-712 DOMAIN SEPARATOR CONSTANTS + bytes32 private constant DOMAIN_TYPE_HASH = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ); + bytes32 private constant RECEIPT_TYPE_HASH = keccak256("Receipt(bytes32 paymentId,bytes32 data)"); + + bytes32 private constant DOMAIN_NAME_HASH = keccak256("Connext Signed Transfer"); + bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); + bytes32 + private constant DOMAIN_SALT = 0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2; + + function recoverSigner(Action memory action, AppState memory state) + public + pure + returns (address) + { + return + ECDSA.recover( + keccak256( + abi.encodePacked( + "\x19\x01", keccak256( - abi.encodePacked( - "\x19\x01", - keccak256( - abi.encode( - DOMAIN_TYPE_HASH, - DOMAIN_NAME_HASH, - DOMAIN_VERSION_HASH, - state.chainId, - state.verifyingContract, - DOMAIN_SALT - ) - ), - keccak256( - abi.encode( - RECEIPT_TYPE_HASH, - state.paymentId, - action.data - ) - ) - ) + abi.encode( + DOMAIN_TYPE_HASH, + DOMAIN_NAME_HASH, + DOMAIN_VERSION_HASH, + state.chainId, + state.verifyingContract, + DOMAIN_SALT + ) ), - action.signature - ); - } - - - function applyAction( - bytes calldata encodedState, - bytes calldata encodedAction - ) - override - external - view - returns (bytes memory) - { - AppState memory state = abi.decode(encodedState, (AppState)); - Action memory action = abi.decode(encodedAction, (Action)); - - require(!state.finalized, "Cannot take action on finalized state"); - - require(state.signerAddress == recoverSigner(action, state), "Incorrect signer recovered from signature"); - - state.coinTransfers[1].amount = state.coinTransfers[0].amount; - state.coinTransfers[0].amount = 0; - state.finalized = true; - - return abi.encode(state); - } - - function computeOutcome(bytes calldata encodedState) - override - external - view - returns (bytes memory) - { - AppState memory state = abi.decode(encodedState, (AppState)); - - return abi.encode(state.coinTransfers); - } - - function getTurnTaker( - bytes calldata /* encodedState */, - address[] calldata participants - ) - override - external - view - returns (address) - { - return participants[1]; // receiver should always be indexed at [1] - } - - function isStateTerminal(bytes calldata encodedState) - override - external - view - returns (bool) - { - AppState memory state = abi.decode(encodedState, (AppState)); - return state.finalized; - } + keccak256(abi.encode(RECEIPT_TYPE_HASH, state.paymentId, action.data)) + ) + ), + action.signature + ); + } + + function applyAction(bytes calldata encodedState, bytes calldata encodedAction) + external + override + view + returns (bytes memory) + { + AppState memory state = abi.decode(encodedState, (AppState)); + Action memory action = abi.decode(encodedAction, (Action)); + + require(!state.finalized, "Cannot take action on finalized state"); + + require( + state.signerAddress == recoverSigner(action, state), + "Incorrect signer recovered from signature" + ); + + state.coinTransfers[1].amount = state.coinTransfers[0].amount; + state.coinTransfers[0].amount = 0; + state.finalized = true; + + return abi.encode(state); + } + + function computeOutcome(bytes calldata encodedState) + external + override + view + returns (bytes memory) + { + AppState memory state = abi.decode(encodedState, (AppState)); + + return abi.encode(state.coinTransfers); + } + + function getTurnTaker( + bytes calldata, /* encodedState */ + address[] calldata participants + ) external override view returns (address) { + return participants[1]; // receiver should always be indexed at [1] + } + + function isStateTerminal(bytes calldata encodedState) external override view returns (bool) { + AppState memory state = abi.decode(encodedState, (AppState)); + return state.finalized; + } } From b9ef947fd42e2971682129ab5b949b82ae0fcbf8 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 16:24:17 -0700 Subject: [PATCH 29/36] handle cancellation explicitly --- .../src.sol/apps/GraphSignedTransferApp.sol | 8 +++++++ .../src.sol/apps/HashLockTransferApp.sol | 22 ++++++++++++++----- .../src.sol/apps/SimpleLinkedTransferApp.sol | 11 +++++++++- .../src.sol/apps/SimpleSignedTransferApp.sol | 8 +++++++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/modules/contracts/src.sol/apps/GraphSignedTransferApp.sol b/modules/contracts/src.sol/apps/GraphSignedTransferApp.sol index f29bd7bd1c..234cd3e2b9 100644 --- a/modules/contracts/src.sol/apps/GraphSignedTransferApp.sol +++ b/modules/contracts/src.sol/apps/GraphSignedTransferApp.sol @@ -91,6 +91,14 @@ contract GraphSignedTransferApp is CounterfactualApp { require(!state.finalized, "Cannot take action on finalized state"); + // Handle cancellation + if (action.responseCID == bytes32(0)) { + state.finalized = true; + + return abi.encode(state); + } + + // Handle payment require( state.signerAddress == recoverAttestationSigner(action, state), "Incorrect signer recovered from signature" diff --git a/modules/contracts/src.sol/apps/HashLockTransferApp.sol b/modules/contracts/src.sol/apps/HashLockTransferApp.sol index 073ce4362c..8355ab5f07 100644 --- a/modules/contracts/src.sol/apps/HashLockTransferApp.sol +++ b/modules/contracts/src.sol/apps/HashLockTransferApp.sol @@ -34,16 +34,26 @@ contract HashLockTransferApp is CounterfactualApp { { AppState memory state = abi.decode(encodedState, (AppState)); Action memory action = abi.decode(encodedAction, (Action)); - bytes32 generatedHash = sha256(abi.encode(action.preImage)); require(!state.finalized, "Cannot take action on finalized state"); require(block.number < state.expiry, "Cannot take action if expiry is expired"); - if (state.lockHash == generatedHash) { - // correct preimage, send payment to receiver - state.coinTransfers[1].amount = state.coinTransfers[0].amount; - state.coinTransfers[0].amount = 0; - state.preImage = action.preImage; + + // Handle cancellation + if (action.preImage == bytes32(0)) { + state.finalized = true; + + return abi.encode(state); } + + // Handle payment + bytes32 generatedHash = sha256(abi.encode(action.preImage)); + require( + state.lockHash == generatedHash, + "Hash generated from preimage does not match hash in state" + ); + state.coinTransfers[1].amount = state.coinTransfers[0].amount; + state.coinTransfers[0].amount = 0; + state.preImage = action.preImage; state.finalized = true; return abi.encode(state); diff --git a/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol b/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol index 68a4582c34..1abe7fa78c 100644 --- a/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol +++ b/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol @@ -31,9 +31,18 @@ contract SimpleLinkedTransferApp is CounterfactualApp { { AppState memory state = abi.decode(encodedState, (AppState)); Action memory action = abi.decode(encodedAction, (Action)); - bytes32 generatedHash = sha256(abi.encode(action.preImage)); require(!state.finalized, "Cannot take action on finalized state"); + + // Handle cancellation + if (action.preImage == bytes32(0)) { + state.finalized = true; + + return abi.encode(state); + } + + // Handle payment + bytes32 generatedHash = sha256(abi.encode(action.preImage)); require( state.linkedHash == generatedHash, "Hash generated from preimage does not match hash in state" diff --git a/modules/contracts/src.sol/apps/SimpleSignedTransferApp.sol b/modules/contracts/src.sol/apps/SimpleSignedTransferApp.sol index a85f4327ef..c0ee481392 100644 --- a/modules/contracts/src.sol/apps/SimpleSignedTransferApp.sol +++ b/modules/contracts/src.sol/apps/SimpleSignedTransferApp.sol @@ -78,6 +78,14 @@ contract SimpleSignedTransferApp is CounterfactualApp { require(!state.finalized, "Cannot take action on finalized state"); + // Handle cancellation + if (action.data == bytes32(0)) { + state.finalized = true; + + return abi.encode(state); + } + + // Handle payment require( state.signerAddress == recoverSigner(action, state), "Incorrect signer recovered from signature" From d33884f16d373597b39c30a8333b27e9d8a193e1 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 16:45:38 -0700 Subject: [PATCH 30/36] set preimage on cancel --- modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol b/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol index 1abe7fa78c..074c995f92 100644 --- a/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol +++ b/modules/contracts/src.sol/apps/SimpleLinkedTransferApp.sol @@ -36,6 +36,7 @@ contract SimpleLinkedTransferApp is CounterfactualApp { // Handle cancellation if (action.preImage == bytes32(0)) { + state.preImage = action.preImage; state.finalized = true; return abi.encode(state); From a928d4543d569fa5d2f3342dfbb6250f2fbb5ef1 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 16:46:21 -0700 Subject: [PATCH 31/36] add cancellation tests --- .../apps/graph-signed-transfer-app.spec.ts | 44 ++++++++++++++++++- .../tests/apps/hashlock-transfer-app.spec.ts | 14 +++++- .../apps/simple-linked-transfer-app.spec.ts | 27 +++++++++--- .../apps/simple-signed-transfer-app.spec.ts | 38 +++++++++++++++- 4 files changed, 113 insertions(+), 10 deletions(-) diff --git a/modules/contracts/src.ts/tests/apps/graph-signed-transfer-app.spec.ts b/modules/contracts/src.ts/tests/apps/graph-signed-transfer-app.spec.ts index d589e88160..ed5430f87f 100644 --- a/modules/contracts/src.ts/tests/apps/graph-signed-transfer-app.spec.ts +++ b/modules/contracts/src.ts/tests/apps/graph-signed-transfer-app.spec.ts @@ -21,7 +21,7 @@ import { GraphSignedTransferApp } from "../../artifacts"; import { expect, provider } from "../utils"; -const { Zero } = constants; +const { HashZero, Zero } = constants; const { defaultAbiCoder } = utils; function mkAddress(prefix: string = "0xa"): string { @@ -166,6 +166,48 @@ describe("GraphSignedTransferApp", () => { validateOutcome(ret, expectedPostState); }); + it("will cancel a payment if an empty action is given", async () => { + const action: GraphSignedTransferAppAction = { + ...receipt, + responseCID: HashZero, + signature: goodSig, + }; + + let ret = await applyAction(preState, action); + const afterActionState = decodeAppState(ret); + + const expectedPostState: GraphSignedTransferAppState = { + coinTransfers: [ + { + amount: transferAmount, + to: senderAddr, + }, + { + amount: Zero, + to: receiverAddr, + }, + ], + paymentId, + signerAddress, + chainId, + verifyingContract, + requestCID: receipt.requestCID, + subgraphDeploymentID: receipt.subgraphDeploymentID, + finalized: true, + }; + + expect(afterActionState.finalized).to.eq(expectedPostState.finalized); + expect(afterActionState.coinTransfers[0].amount).to.eq( + expectedPostState.coinTransfers[0].amount, + ); + expect(afterActionState.coinTransfers[1].amount).to.eq( + expectedPostState.coinTransfers[1].amount, + ); + + ret = await computeOutcome(afterActionState); + validateOutcome(ret, expectedPostState); + }); + it("will revert action with incorrect signature", async () => { const action: GraphSignedTransferAppAction = { ...receipt, diff --git a/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts b/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts index f7db9d53c3..d1aecc5896 100644 --- a/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts +++ b/modules/contracts/src.ts/tests/apps/hashlock-transfer-app.spec.ts @@ -147,9 +147,9 @@ describe("HashLockTransferApp", () => { validateOutcome(ret, expectedPostState); }); - it("will not redeem a payment if an incorrect hash is given", async () => { + it("will cancel a payment if an empty action is given", async () => { const action: HashLockTransferAppAction = { - preImage: getRandomBytes32(), // incorrect hash + preImage: HashZero, // cancel hash }; let ret = await applyAction(preState, action); @@ -184,6 +184,16 @@ describe("HashLockTransferApp", () => { validateOutcome(ret, expectedPostState); }); + it("will not redeem a payment if an incorrect hash is given", async () => { + const action: HashLockTransferAppAction = { + preImage: getRandomBytes32(), // incorrect hash + }; + + await expect(applyAction(preState, action)).revertedWith( + "Hash generated from preimage does not match hash in state", + ); + }); + it("will revert action if already finalized", async () => { const action: HashLockTransferAppAction = { preImage, diff --git a/modules/contracts/src.ts/tests/apps/simple-linked-transfer-app.spec.ts b/modules/contracts/src.ts/tests/apps/simple-linked-transfer-app.spec.ts index 4eeb9239ac..3cd2686530 100644 --- a/modules/contracts/src.ts/tests/apps/simple-linked-transfer-app.spec.ts +++ b/modules/contracts/src.ts/tests/apps/simple-linked-transfer-app.spec.ts @@ -13,7 +13,7 @@ import { SimpleLinkedTransferApp } from "../../artifacts"; import { expect, provider } from "../utils"; -const { Zero } = constants; +const { HashZero, Zero } = constants; const { defaultAbiCoder, soliditySha256 } = utils; const decodeTransfers = (encodedAppState: string): CoinTransfer[] => @@ -95,14 +95,19 @@ describe("SimpleLinkedTransferApp", () => { pre: SimpleLinkedTransferAppState, post: SimpleLinkedTransferAppState, action: SimpleLinkedTransferAppAction, + success: boolean = true, ) => { - expect(post.preImage).to.eq(action.preImage); - expect(post.finalized).to.be.true; - expect(post.linkedHash).to.eq(pre.linkedHash); - expect(post.coinTransfers[0].amount).to.eq(Zero); expect(post.coinTransfers[0].to).to.eq(pre.coinTransfers[0].to); - expect(post.coinTransfers[1].amount).to.eq(pre.coinTransfers[0].amount); expect(post.coinTransfers[1].to).to.eq(pre.coinTransfers[1].to); + expect(post.finalized).to.be.true; + expect(post.preImage).to.eq(action.preImage); + if (success) { + expect(post.coinTransfers[0].amount).to.eq(Zero); + expect(post.coinTransfers[1].amount).to.eq(pre.coinTransfers[0].amount); + return; + } + expect(post.coinTransfers[0].amount).to.eq(pre.coinTransfers[0].amount); + expect(post.coinTransfers[1].amount).to.eq(Zero); }; before(async () => { @@ -124,6 +129,16 @@ describe("SimpleLinkedTransferApp", () => { await validateOutcome(afterActionState, outcome); }); + it("can cancel a payment", async () => { + const preImage = HashZero; + const initialState = await createInitialState(preImage); + const action: SimpleLinkedTransferAppAction = { preImage }; + const afterActionState = await applyAction(initialState, action); + await validateAction(initialState, afterActionState, action, false); + const outcome = await computeOutcome(afterActionState); + await validateOutcome(afterActionState, outcome); + }); + it("refunds a payment if app state is not finalized", async () => { const preImage = getRandomBytes32(); const initialState = await createInitialState(preImage); diff --git a/modules/contracts/src.ts/tests/apps/simple-signed-transfer-app.spec.ts b/modules/contracts/src.ts/tests/apps/simple-signed-transfer-app.spec.ts index 31aea48fdb..db7e7d09cc 100644 --- a/modules/contracts/src.ts/tests/apps/simple-signed-transfer-app.spec.ts +++ b/modules/contracts/src.ts/tests/apps/simple-signed-transfer-app.spec.ts @@ -21,7 +21,7 @@ import { SimpleSignedTransferApp } from "../../artifacts"; import { expect, provider } from "../utils"; -const { Zero } = constants; +const { HashZero, Zero } = constants; const { defaultAbiCoder } = utils; function mkAddress(prefix: string = "0xa"): string { @@ -165,6 +165,42 @@ describe("SimpleSignedTransferApp", () => { validateOutcome(ret, expectedPostState); }); + it("will cancel a payment", async () => { + const action: SimpleSignedTransferAppAction = { + data: HashZero, + signature: goodSig, + }; + + let ret = await applyAction(preState, action); + const afterActionState = decodeAppState(ret); + + const expectedPostState: SimpleSignedTransferAppState = { + ...preState, + coinTransfers: [ + { + amount: transferAmount, + to: senderAddr, + }, + { + amount: Zero, + to: receiverAddr, + }, + ], + finalized: true, + }; + + expect(afterActionState.finalized).to.eq(expectedPostState.finalized); + expect(afterActionState.coinTransfers[0].amount).to.eq( + expectedPostState.coinTransfers[0].amount, + ); + expect(afterActionState.coinTransfers[1].amount).to.eq( + expectedPostState.coinTransfers[1].amount, + ); + + ret = await computeOutcome(afterActionState); + validateOutcome(ret, expectedPostState); + }); + it("will revert action with incorrect signature", async () => { const action: SimpleSignedTransferAppAction = { data, From 006a7c9590739509ace125436ee5509353f74e0c Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 16:55:09 -0700 Subject: [PATCH 32/36] allow cancellation before expiry --- modules/contracts/src.sol/apps/HashLockTransferApp.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/contracts/src.sol/apps/HashLockTransferApp.sol b/modules/contracts/src.sol/apps/HashLockTransferApp.sol index 8355ab5f07..8c60b21945 100644 --- a/modules/contracts/src.sol/apps/HashLockTransferApp.sol +++ b/modules/contracts/src.sol/apps/HashLockTransferApp.sol @@ -36,7 +36,6 @@ contract HashLockTransferApp is CounterfactualApp { Action memory action = abi.decode(encodedAction, (Action)); require(!state.finalized, "Cannot take action on finalized state"); - require(block.number < state.expiry, "Cannot take action if expiry is expired"); // Handle cancellation if (action.preImage == bytes32(0)) { @@ -46,6 +45,9 @@ contract HashLockTransferApp is CounterfactualApp { } // Handle payment + // Check here to always allow cancellation of a payment if the payment + // itself has expired + require(block.number < state.expiry, "Cannot take action if expiry is expired"); bytes32 generatedHash = sha256(abi.encode(action.preImage)); require( state.lockHash == generatedHash, From f34ff43d0e758a77937292b352fd89939c1dcd40 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 16:55:36 -0700 Subject: [PATCH 33/36] remove special case flow --- modules/client/src/connext.ts | 6 ++ .../controllers/ResolveTransferController.ts | 69 ++--------------- .../src/appRegistry/appRegistry.service.ts | 74 ------------------- modules/node/src/listener/listener.service.ts | 13 +--- modules/types/src/public.ts | 2 +- 5 files changed, 17 insertions(+), 147 deletions(-) diff --git a/modules/client/src/connext.ts b/modules/client/src/connext.ts index 98e516a1a3..2955df23b0 100644 --- a/modules/client/src/connext.ts +++ b/modules/client/src/connext.ts @@ -325,6 +325,12 @@ export class ConnextClient implements IConnextClient { public resolveCondition = async ( params: PublicParams.ResolveCondition, ): Promise => { + // paymentId is generated for hashlock transfer + if (params.conditionType === ConditionalTransferTypes.HashLockTransfer && !params.paymentId) { + const lockHash = soliditySha256(["bytes32"], [params.preImage]); + const paymentId = soliditySha256(["address", "bytes32"], [params.assetId, lockHash]); + params.paymentId = paymentId; + } return this.resolveTransferController.resolveTransfer(params); }; diff --git a/modules/client/src/controllers/ResolveTransferController.ts b/modules/client/src/controllers/ResolveTransferController.ts index a45c753e4b..10cbbeb352 100644 --- a/modules/client/src/controllers/ResolveTransferController.ts +++ b/modules/client/src/controllers/ResolveTransferController.ts @@ -11,12 +11,11 @@ import { GraphSignedTransferAppAction, AppInstanceJson, } from "@connext/types"; -import { stringify, toBN } from "@connext/utils"; -import { BigNumber, constants } from "ethers"; +import { stringify } from "@connext/utils"; +import { BigNumber } from "ethers"; import { AbstractController } from "./AbstractController"; -const { HashZero } = constants; export class ResolveTransferController extends AbstractController { public resolveTransfer = async ( params: PublicParams.ResolveCondition, @@ -51,16 +50,11 @@ export class ResolveTransferController extends AbstractController { // Extract the secret object from the params; if (!this.hasSecret(params)) { - // User is cancelling the payment - try { - this.log.info(`[${paymentId}] Cancelling payment`); - const ret = await this.handleCancellation(params); - this.log.info(`[${paymentId}] resolveCondition complete: ${stringify(ret)}`); - return ret; - } catch (e) { - emitFailureEvent(e); - throw e; - } + const error = new Error( + `Cannot resolve payment without providing a secret. Params: ${stringify(params)}`, + ); + emitFailureEvent(error); + throw error; } // Install app with receiver @@ -253,53 +247,4 @@ export class ResolveTransferController extends AbstractController { } throw new Error(`Invalid condition type: ${conditionType}`); } - - private async handleCancellation( - params: PublicParams.ResolveCondition, - ): Promise { - const { conditionType, paymentId } = params; - const appDefinition = this.connext.appRegistry.find((app) => app.name === conditionType) - .appDefinitionAddress; - const apps = await this.connext.getAppInstances(); - const paymentApp = apps.find((app) => { - const participants = (app.latestState as GenericConditionalTransferAppState).coinTransfers.map( - (t) => t.to, - ); - return ( - app.appDefinition === appDefinition && - app.meta.paymentId === paymentId && - participants.includes(this.connext.signerAddress) - ); - }); - - if (!paymentApp) { - throw new Error(`Cannot find payment associated with ${paymentId}`); - } - - const ret = { - appIdentityHash: paymentApp.identityHash, - amount: (paymentApp.latestState as GenericConditionalTransferAppState).coinTransfers[0] - .amount, - assetId: paymentApp.outcomeInterpreterParameters["tokenAddress"], - meta: paymentApp.meta, - paymentId: params.paymentId, - sender: paymentApp.meta.sender, - }; - - let action: HashLockTransferAppAction | undefined = undefined; - // IFF payment app has an expiry, the app has not expired, and the user - // is the receiver of the payment, play an invalid action - const state = paymentApp.latestState as any; - const block = await this.ethProvider.getBlockNumber(); - if ( - state.coinTransfers[1].to === this.connext.signerAddress && - toBN(state.expiry || 0).gt(block) - ) { - action = { preImage: HashZero }; - } - this.log.info(`[${paymentId}] Uninstalling transfer app with action ${stringify(action)}`); - await this.connext.uninstallApp(paymentApp.identityHash, action); - this.log.info(`[${paymentId}] Uninstalled transfer app ${paymentApp.identityHash}`); - return ret; - } } diff --git a/modules/node/src/appRegistry/appRegistry.service.ts b/modules/node/src/appRegistry/appRegistry.service.ts index 492cad6042..70fea183f9 100644 --- a/modules/node/src/appRegistry/appRegistry.service.ts +++ b/modules/node/src/appRegistry/appRegistry.service.ts @@ -184,80 +184,6 @@ export class AppRegistryService implements OnModuleInit { ); } - // should handle: - // - cancellation cases of conditional payments - public async handleAppUninstall( - appName: SupportedApplicationNames, - app: AppInstanceJson, - latestState: AppState, - ): Promise { - this.log.info( - `handleAppUninstall for ${appName} ${app.identityHash}, latest state ${safeJsonStringify( - latestState, - )} started`, - ); - // if the uninstalled app was a cancelled transfer app or - // payment, uninstall corresponding sender/receiver payment - if (!Object.keys(ConditionalTransferAppNames).includes(appName)) { - this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); - return; - } - - const state = latestState as GenericConditionalTransferAppState; - const receiverAppUninstalled = - state.coinTransfers[0].to === (await this.configService.getSignerAddress()); - - if (toBN(state.coinTransfers[0].amount).isZero()) { - // payment is not being cancelled, nothing to handle on uninstall - this.log.info(`Payment was uninstalled but not cancelled, doing nothing.`); - this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); - return; - } - - // get the second leg for the payment (either party can cancel payment) - const secondLeg = receiverAppUninstalled - ? await this.transferService.findSenderAppByPaymentId(app.meta.paymentId) - : await this.transferService.findReceiverAppByPaymentId(app.meta.paymentId); - if (!secondLeg) { - return; - } - - // check case where sender app is cancelled, receiver never had proposal - if (secondLeg.type !== AppType.INSTANCE) { - if (!receiverAppUninstalled && secondLeg.type === AppType.PROPOSAL) { - this.log.info(`Sender cancelled payment, rejecting receiver proposal`); - await this.cfCoreService.rejectInstallApp( - secondLeg.identityHash, - secondLeg.channel.multisigAddress, - "Sender cancelled payment", - ); - this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); - return; - } - // otherwise no second leg installed, return - this.log.info(`No second leg found for cancelled transfer, doing nothing`); - return; - } - - // Proceed with cancelling second leg of payment - this.log.info( - `Payment uninstalled without balance change uninstalling second leg of payment ${secondLeg.identityHash}`, - ); - // handle the case where this is an unexpired sendeder app - let action: HashLockTransferAppAction | undefined = undefined; - const block = await this.configService.getEthProvider().getBlockNumber(); - if (secondLeg.latestState.expiry && toBN(secondLeg.latestState.expiry).gt(block)) { - this.log.info(`Second leg of payment not yet expired, uninstalling with invalid action`); - action = { preImage: HashZero }; - } - await this.cfCoreService.uninstallApp( - secondLeg.identityHash, - secondLeg.channel.multisigAddress, - action, - ); - this.log.info(`handleAppUninstall for app name ${appName} ${app.identityHash} complete`); - } - // APP SPECIFIC MIDDLEWARE public generateMiddleware = async (): Promise< (protocol: ProtocolName, cxt: MiddlewareContext) => Promise diff --git a/modules/node/src/listener/listener.service.ts b/modules/node/src/listener/listener.service.ts index 272b6005b9..f56e8f1a15 100644 --- a/modules/node/src/listener/listener.service.ts +++ b/modules/node/src/listener/listener.service.ts @@ -16,7 +16,6 @@ import { LoggerService } from "../logger/logger.service"; import { AppActionsService } from "../appRegistry/appActions.service"; import { AppInstanceRepository } from "../appInstance/appInstance.repository"; import { ChannelRepository } from "../channel/channel.repository"; -import { stringify } from "@connext/utils"; const { CONDITIONAL_TRANSFER_CREATED_EVENT, @@ -185,22 +184,16 @@ export default class ListenerService implements OnModuleInit { return; } const channel = await this.channelRepository.findByMultisigAddressOrThrow(multisigAddress); - const appRegistryInfo = this.cfCoreService.getAppInfoByAppDefinitionAddress( - uninstalledApp.appDefinition, - ); if (action) { + const appRegistryInfo = this.cfCoreService.getAppInfoByAppDefinitionAddress( + uninstalledApp.appDefinition, + ); await this.appActionsService.handleAppAction( appRegistryInfo.name as SupportedApplicationNames, uninstalledApp, uninstalledApp.latestState as any, // AppState (excluding simple swap app) action as AppAction, ); - } else { - await this.appRegistryService.handleAppUninstall( - appRegistryInfo.name as SupportedApplicationNames, - uninstalledApp, - uninstalledApp.latestState as any, // AppState (excluding simple swap app) - ); } const assetIdResponder = ( diff --git a/modules/types/src/public.ts b/modules/types/src/public.ts index c37a18f1ab..540e0730f2 100644 --- a/modules/types/src/public.ts +++ b/modules/types/src/public.ts @@ -53,7 +53,7 @@ type HashLockTransferResponse = { type ResolveHashLockTransferParameters = { conditionType: typeof ConditionalTransferTypes.HashLockTransfer; assetId: Address; - paymentId: Bytes32; + paymentId?: Bytes32; preImage?: Bytes32; }; From 3e6f0eddcf111d03da7796b3eb4ac87ba6222c96 Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 17:21:15 -0700 Subject: [PATCH 34/36] fix types --- .../src/transfer/hashLockTransfer.test.ts | 17 +++++++++++------ modules/types/src/public.ts | 8 ++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/modules/test-runner/src/transfer/hashLockTransfer.test.ts b/modules/test-runner/src/transfer/hashLockTransfer.test.ts index 4fca4f041a..4ca01bacb9 100644 --- a/modules/test-runner/src/transfer/hashLockTransfer.test.ts +++ b/modules/test-runner/src/transfer/hashLockTransfer.test.ts @@ -309,12 +309,14 @@ describe("HashLock Transfers", () => { const { paymentId } = await sendHashlockTransfer(clientA, clientB, opts); const badPreImage = getRandomBytes32(); - await clientB.resolveCondition({ - conditionType: ConditionalTransferTypes.HashLockTransfer, - preImage: badPreImage, - paymentId: paymentId!, - assetId: transfer.assetId, - } as PublicParams.ResolveHashLockTransfer); + await expect( + clientB.resolveCondition({ + conditionType: ConditionalTransferTypes.HashLockTransfer, + preImage: badPreImage, + paymentId: paymentId!, + assetId: transfer.assetId, + } as PublicParams.ResolveHashLockTransfer), + ).to.be.rejectedWith(/Hash generated from preimage does not match hash in state/); // verfy payment did not go through const { [clientB.signerAddress]: receiverBal } = await clientB.getFreeBalance(transfer.assetId); @@ -394,6 +396,7 @@ describe("HashLock Transfers", () => { clientB .resolveCondition({ paymentId: paymentId!, + preImage: HashZero, conditionType: ConditionalTransferTypes.HashLockTransfer, assetId: transfer.assetId, }) @@ -422,6 +425,7 @@ describe("HashLock Transfers", () => { clientB .resolveCondition({ paymentId: paymentId!, + preImage: HashZero, conditionType: ConditionalTransferTypes.HashLockTransfer, assetId: transfer.assetId, }) @@ -464,6 +468,7 @@ describe("HashLock Transfers", () => { await clientA.resolveCondition({ paymentId: paymentId!, + preImage: HashZero, conditionType: ConditionalTransferTypes.HashLockTransfer, assetId: transfer.assetId, }); diff --git a/modules/types/src/public.ts b/modules/types/src/public.ts index 540e0730f2..3d1ac41c39 100644 --- a/modules/types/src/public.ts +++ b/modules/types/src/public.ts @@ -54,7 +54,7 @@ type ResolveHashLockTransferParameters = { conditionType: typeof ConditionalTransferTypes.HashLockTransfer; assetId: Address; paymentId?: Bytes32; - preImage?: Bytes32; + preImage: Bytes32; }; type ResolveHashLockTransferResponse = { @@ -87,7 +87,7 @@ type LinkedTransferResponse = { type ResolveLinkedTransferParameters = { conditionType: typeof ConditionalTransferTypes.LinkedTransfer; paymentId: Bytes32; - preImage?: Bytes32; + preImage: Bytes32; }; type ResolveLinkedTransferResponse = { @@ -122,7 +122,7 @@ type SignedTransferResponse = { type ResolveSignedTransferParameters = { conditionType: typeof ConditionalTransferTypes.SignedTransfer; paymentId: Bytes32; - data?: Bytes32; + data: Bytes32; signature?: SignatureString; }; @@ -159,7 +159,7 @@ type GraphSignedTransferResponse = { type ResolveGraphSignedTransferParameters = { conditionType: typeof ConditionalTransferTypes.GraphTransfer; paymentId: Bytes32; - responseCID?: Bytes32; + responseCID: Bytes32; signature?: SignatureString; }; From b07b3235af69677d6028153e85a1565bb1eefe6b Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 17:24:56 -0700 Subject: [PATCH 35/36] make reset --- modules/contracts/address-book.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/contracts/address-book.json b/modules/contracts/address-book.json index 3188d0084c..4b45b9b9de 100644 --- a/modules/contracts/address-book.json +++ b/modules/contracts/address-book.json @@ -636,9 +636,9 @@ }, "HashLockTransferApp": { "address": "0x8f0483125FCb9aaAEFA9209D8E9d7b9C8B9Fb90F", - "creationCodeHash": "0x3c7dca6d6adb002d5fe9430ee5f5b6d1150a3dc2686aedde8e899ea59848b1e5", - "runtimeCodeHash": "0xc9abe7901965be973f7c944578049a100b7726859dd0ac87f96ee3ca397f89e6", - "txHash": "0xde79991981bd38286454f95a413a5c4f92128042c90ebc12c7bb78bac1d1dc59" + "creationCodeHash": "0xdd7335a25e79a40affc32e0bee3afc37ff2426833a5b2fd99b3019be34aa26d0", + "runtimeCodeHash": "0x3854fc4d1acaebd553bf5bde3711bc6519231d05da9782b3fd37f568b0e98cfb", + "txHash": "0x10aac0d12118c997d41085c935590753741531bc39e1aa66870d5c07f2739323" }, "IdentityApp": { "address": "0x9FBDa871d559710256a2502A2517b794B482Db40", @@ -666,15 +666,15 @@ }, "SimpleLinkedTransferApp": { "address": "0xAa588d3737B611baFD7bD713445b314BD453a5C8", - "creationCodeHash": "0xd7654b970372f59bfe7bd4ce192cf4c72fd3c06f3ba172f684ecf7941911f506", - "runtimeCodeHash": "0x420606984886ac3ffcbc01a0e36addfb6041a63535557bdf9ab09ed51436d15d", - "txHash": "0x9d8856fa03f96a54fd5f2dd176e77fc1733baaa724d44d1905e298de9e3e6727" + "creationCodeHash": "0x11e378f0278ab7fc0b7816860feb29f5b31e6ac4072e7013714490ea1d4c3417", + "runtimeCodeHash": "0xe3ab03d23b524230a67b7f88ed82e10cd57a6173b0d1548d1be275b3f989cc99", + "txHash": "0xf305dfe137f11510924f011a027f123f23336bea7d9dd7c4f4858774c828140e" }, "SimpleSignedTransferApp": { "address": "0xf204a4Ef082f5c04bB89F7D5E6568B796096735a", - "creationCodeHash": "0x607fdb378f50f76d63f28bc450489943867b284ee532f35cffabea3e1245e437", - "runtimeCodeHash": "0x76de51dd29331977b6d89ac308f0f10f4baa4efc13def8f72ea8560b05705b0f", - "txHash": "0xd5aaad661a787cacf028e546db767186d8cabbcfc53b94681cbb8622399c94d4" + "creationCodeHash": "0xd6354b69be5f545c34925ad1b6be5eb5b28abfe904e907563ea01af3e529a4a3", + "runtimeCodeHash": "0xf85cf0f22b5d8ebeaa0a716d7aa62cff5804238c8fb45e344541605d3fffe41a", + "txHash": "0x3cefd8eab3a6aed72307e479e2effc4b90d2e8931a1722b4c548c1ad052359b0" }, "SimpleTwoPartySwapApp": { "address": "0x75c35C980C0d37ef46DF04d31A140b65503c0eEd", @@ -720,9 +720,9 @@ }, "GraphSignedTransferApp": { "address": "0xf25186B5081Ff5cE73482AD761DB0eB0d25abfBF", - "creationCodeHash": "0x40ddb8d45b06fbf5c6c0afb4405234477a0415f51b19d04dc2fbe455835ff8af", - "runtimeCodeHash": "0xfa5f77d416a82710cfc3fc70d4c46956be74c9179cf34e3dbbbc7d992781da20", - "txHash": "0xc8511b85706c9a25ee3bb0b696871ba70970fc4448ebdd72244bb5da146fa87f" + "creationCodeHash": "0x15c3697bbebe896d74092fd977a44c576e3e54046c01103cd481c381c4bf14d2", + "runtimeCodeHash": "0x76797dc9a7061a3d5fc10f961d93e9a60679f81c9201c3ddb40d2f63abcb2ef5", + "txHash": "0x3bf830d3523aff1cdd10ef1a9ea177a6b3edb5f1b51c880a6de856128f4429a4" } } } \ No newline at end of file From 6219d8b4e768e86108ebf9a265337d84f4822dda Mon Sep 17 00:00:00 2001 From: LayneHaber Date: Mon, 13 Jul 2020 19:46:43 -0700 Subject: [PATCH 36/36] fix build --- modules/test-runner/src/transfer/hashLockTransfer.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/test-runner/src/transfer/hashLockTransfer.test.ts b/modules/test-runner/src/transfer/hashLockTransfer.test.ts index fb2032d6ea..5efe4ec976 100644 --- a/modules/test-runner/src/transfer/hashLockTransfer.test.ts +++ b/modules/test-runner/src/transfer/hashLockTransfer.test.ts @@ -493,6 +493,8 @@ describe("HashLock Transfers", () => { preImage: HashZero, }, ); + }); + // FIXME: may not work depending on collateral, will expect some payment // errors even with a small number of payments until this is handled better it.skip("can send concurrent hashlock transfers", async () => {