diff --git a/db/migrations/postgres/000116_tx_type_not_null.down.sql b/db/migrations/postgres/000116_tx_type_not_null.down.sql new file mode 100644 index 000000000..af751ba0d --- /dev/null +++ b/db/migrations/postgres/000116_tx_type_not_null.down.sql @@ -0,0 +1,3 @@ +BEGIN; +ALTER TABLE messages ALTER COLUMN tx_parent_type DROP NOT NULL; +COMMIT; diff --git a/db/migrations/postgres/000116_tx_type_not_null.up.sql b/db/migrations/postgres/000116_tx_type_not_null.up.sql new file mode 100644 index 000000000..6030375bc --- /dev/null +++ b/db/migrations/postgres/000116_tx_type_not_null.up.sql @@ -0,0 +1,5 @@ +BEGIN; +UPDATE messages SET tx_parent_type = '' + WHERE tx_parent_type IS NULL; +ALTER TABLE messages ALTER COLUMN tx_parent_type SET NOT NULL; +COMMIT; diff --git a/db/migrations/sqlite/000116_tx_type_not_null.down.sql b/db/migrations/sqlite/000116_tx_type_not_null.down.sql new file mode 100644 index 000000000..892a01e79 --- /dev/null +++ b/db/migrations/sqlite/000116_tx_type_not_null.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE messages RENAME COLUMN tx_parent_type TO tx_parent_type_temp; +ALTER TABLE messages ADD COLUMN tx_parent_type VARCHAR(64); +UPDATE messages SET tx_parent_type = tx_parent_type_temp; +ALTER TABLE messages DROP COLUMN tx_parent_type_temp; \ No newline at end of file diff --git a/db/migrations/sqlite/000116_tx_type_not_null.up.sql b/db/migrations/sqlite/000116_tx_type_not_null.up.sql new file mode 100644 index 000000000..d76110881 --- /dev/null +++ b/db/migrations/sqlite/000116_tx_type_not_null.up.sql @@ -0,0 +1,5 @@ +UPDATE messages SET tx_parent_type = '' WHERE tx_parent_type IS NULL; +ALTER TABLE messages RENAME COLUMN tx_parent_type TO tx_parent_type_temp; +ALTER TABLE messages ADD COLUMN tx_parent_type VARCHAR(64) DEFAULT '' NOT NULL; +UPDATE messages SET tx_parent_type = tx_parent_type_temp; +ALTER TABLE messages DROP COLUMN tx_parent_type_temp; \ No newline at end of file diff --git a/go.work.sum b/go.work.sum index a7711c89e..ee356555a 100644 --- a/go.work.sum +++ b/go.work.sum @@ -162,293 +162,97 @@ cloud.google.com/go/gkeconnect v0.8.4/go.mod h1:84hZz4UMlDCKl8ifVW8layK4WHlMAFeq cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= cloud.google.com/go/gkehub v0.14.4/go.mod h1:Xispfu2MqnnFt8rV/2/3o73SK1snL8s9dYJ9G2oQMfc= cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/gsuiteaddons v1.6.4/go.mod h1:rxtstw7Fx22uLOXBpsvb9DUbC+fiXs7rF4U29KHM/pE= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= -cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE= cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/iap v1.9.3/go.mod h1:DTdutSZBqkkOm2HEOTBzhZxh2mwwxshfD/h3yofAiCw= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/ids v1.4.4/go.mod h1:z+WUc2eEl6S/1aZWzwtVNWoSZslgzPxAboS0lZX0HjI= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/iot v1.7.4/go.mod h1:3TWqDVvsddYBG++nHSZmluoCAVGr1hAcabbWZNKEZLk= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.12.2/go.mod h1:9idWapzr/JKXBBQ4lWqVX/hcadxB194ry20m/bTrhWc= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/lifesciences v0.9.4/go.mod h1:bhm64duKhMi7s9jR9WYJYvjAFJwRqNj+Nia7hF0Z7JA= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.5.2/go.mod h1:nqo6DQbNV2pXhGDbDMoN2bWz68MjZUzqv2YttZiveCs= cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/managedidentities v1.6.4/go.mod h1:WgyaECfHmF00t/1Uk8Oun3CQ2PGUtjc3e9Alh79wyiM= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/maps v1.6.1/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/mediatranslation v0.8.4/go.mod h1:9WstgtNVAdN53m6TQa5GjIjLqKQPXe74hwSCxUP6nj4= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.10.4/go.mod h1:v/d8PuC8d1gD6Yn5+I3INzLR01IDn0N4Ym56RgikSI0= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/metastore v1.13.3/go.mod h1:K+wdjXdtkdk7AQg4+sXS8bRrQa9gcOr+foOMF2tqINE= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/monitoring v1.16.3/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkconnectivity v1.14.3/go.mod h1:4aoeFdrJpYEXNvrnfyD5kIzs8YtHg945Og4koAjHQek= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networkmanagement v1.9.3/go.mod h1:y7WMO1bRLaP5h3Obm4tey+NquUvB93Co1oh4wpL+XcU= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/networksecurity v0.9.4/go.mod h1:E9CeMZ2zDsNBkr8axKSYm8XyTqNhiCHf1JO/Vb8mD1w= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.11.2/go.mod h1:z0tlHI/lREXC8BS2mIsUeR3agM1AkgLiS+Isov3SS70= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/optimization v1.6.2/go.mod h1:mWNZ7B9/EyMCcwNl1frUGEuY6CPijSkz88Fz2vwKPOY= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orchestration v1.8.4/go.mod h1:d0lywZSVYtIoSZXb0iFjv9SaL13PGyVOKDxqGxEf/qI= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/orgpolicy v1.11.4/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/osconfig v1.12.4/go.mod h1:B1qEwJ/jzqSRslvdOCI8Kdnp0gSng0xW4LOnIebQomA= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.12.2/go.mod h1:CQ3V8Jvw4Qo4WRhNPF0o+HAM4DiLuE27Ul9CX9g2QdY= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/phishingprotection v0.8.4/go.mod h1:6b3kNPAc2AQ6jZfFHioZKg9MQNybDg4ixFd4RPZZ2nE= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/policytroubleshooter v1.10.2/go.mod h1:m4uF3f6LseVEnMV6nknlN2vYGRb+75ylQwJdnOXfnv0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/privatecatalog v0.9.4/go.mod h1:SOjm93f+5hp/U3PqMZAHTtBtluqLygrDrVO8X8tYtG0= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recaptchaenterprise/v2 v2.8.3/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommendationengine v0.8.4/go.mod h1:GEteCf1PATl5v5ZsQ60sTClUE0phbWmo3rQ1Js8louU= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.11.3/go.mod h1:+FJosKKJSId1MBFeJ/TTyoGQZiEelQQIZMKYYD8ruK4= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/redis v1.14.1/go.mod h1:MbmBxN8bEnQI4doZPC1BzADU4HGocHBk2de3SbgOkqs= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcemanager v1.9.4/go.mod h1:N1dhP9RFvo3lUfwtfLWVxfUWq8+KUQ+XLlHLH3BoFJ0= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/resourcesettings v1.6.4/go.mod h1:pYTTkWdv2lmQcjsthbZLNBP4QW140cs7wqA3DuqErVI= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/retail v1.14.4/go.mod h1:l/N7cMtY78yRnJqp5JW8emy7MB1nz8E4t2yfOmklYfg= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/run v1.3.3/go.mod h1:WSM5pGyJ7cfYyYbONVQBN4buz42zFqwG67Q3ch07iK4= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.10.4/go.mod h1:MTuXcrJC9tqOHhixdbHDFSIuh7xZF2IysiINDuiq6NI= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/security v1.15.4/go.mod h1:oN7C2uIZKhxCLiAAijKUCuHLZbIt/ghYEo8MqwD/Ty4= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/securitycenter v1.24.2/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.11.3/go.mod h1:LV+cHkomRLr67YoQy3Xq2tUXBGOs5z5bPofdq7qtiAw= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/shell v1.7.4/go.mod h1:yLeXB8eKLxw0dpEmXQ/FjriYrBijNsONpwnWsdPqlKM= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.51.0/go.mod h1:c5KNo5LQ1X5tJwma9rSQZsXNBDNvj4/n8BVc3LNahq0= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= -cloud.google.com/go/speech v1.20.1/go.mod h1:wwolycgONvfz2EDU8rKuHRW3+wc9ILPsAWoikBEWavY= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.10.3/go.mod h1:Up8LY2p6X68SZ+WToswpQbQHnJpOty/ACcMafuey8gc= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/talent v1.6.5/go.mod h1:Mf5cma696HmE+P2BWJ/ZwYqeJXEeU0UqjHFXVLadEDI= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/texttospeech v1.7.4/go.mod h1:vgv0002WvR4liGuSd5BJbWy4nDn5Ozco0uJymY5+U74= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/tpu v1.6.4/go.mod h1:NAm9q3Rq2wIlGnOhpYICNI7+bpBebMJbh0yyp3aNw1Y= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.10.4/go.mod h1:Nso99EDIK8Mj5/zmB+iGr9dosS/bzWCJ8wGmE6TXNWY= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.9.3/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.20.3/go.mod h1:TnH/mNZKVHeNtpamsSPygSR0iHtvrR/cW1/GDjN5+GU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vision/v2 v2.7.5/go.mod h1:GcviprJLFfK9OLf0z8Gm6lQb6ZFUulvpZws+mm6yPLM= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmmigration v1.7.4/go.mod h1:yBXCmiLaB99hEl/G9ZooNx2GyzgsjKnw5fWcINRgD70= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vmwareengine v1.0.3/go.mod h1:QSpdZ1stlbfKtyt6Iu19M6XRxjmXO+vb5a/R6Fvy2y4= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/vpcaccess v1.7.4/go.mod h1:lA0KTvhtEOb/VOdnH/gwPuOzGgM+CWsmGu6bb4IoMKk= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/webrisk v1.9.4/go.mod h1:w7m4Ib4C+OseSr2GL66m0zMBywdrVNTDKsdEsfMl7X0= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/websecurityscanner v1.6.4/go.mod h1:mUiyMQ+dGpPPRkHgknIZeCzSHJ45+fY4F52nZFDHm2o= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= -github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= -github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs======== -github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8/go.mod h1:JTnlBSot91steJeti4ryyu/tLd4Sk84O5W22L7O2EQU= +github.com/aws/aws-sdk-go-v2/credentials v1.12.20/go.mod h1:UKY5HyIux08bbNA7Blv4PcXQ8cTkGh7ghHMFklaviR4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33/go.mod h1:84XgODVR8uRhmOnUkKGUZKqIMxmjmLOR8Uyp7G/TPwc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14/go.mod h1:AyGgqiKv9ECM6IZeNQtdT8NnMvUb3/2wokeq2Fgryto= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9/go.mod h1:a9j48l6yL5XINLHLcOKInjdvknN+vWqPBxqeIDw7ktw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18/go.mod h1:NS55eQ4YixUJPTC+INxi2/jCqe1y2Uw3rnh9wEOVJxY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17/go.mod h1:YqMdV+gEKCQ59NrB7rzrJdALeBIsYiVi8Inj3+KcqHI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11/go.mod h1:fmgDANqTUCxciViKl9hb/zD5LFbvPINFRgWhDbR+vZo= +github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220520190051-1e77728a1eaa/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw= +github.com/cucumber/godog v0.12.6/go.mod h1:Y02TTpimPXDb70PnG6M3zpODXm1+bjCsuZzcW76xAww= +github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.6.13/go.mod h1:qEySVqXrEugbHKvmhI8ZqtQi75/RHSSRNpffvB4I6Bw= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= -github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.3/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= @@ -466,381 +270,81 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8 github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4/v4 v4.1.16/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= +github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/snowflakedb/gosnowflake v1.6.19/go.mod h1:FM1+PWUdwB9udFDsXdfD58NONC0m+MlOSmQRvimobSM= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8= -go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= -go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4= -go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220907135653-1e95f45603a7/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908150016-7ac13a9a928d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= -google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= -google.golang.org/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg= -google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= -google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= -google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg= google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20231120223509-83a465c0220f/go.mod h1:iIgEblxoG4klcXsG0d9cpoxJ4xndv6+1FkDROCHhPRI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/bson.v2 v2.0.0-20171018101713-d8c8987b8862/go.mod h1:VN8wuk/3Ksp8lVZ82HHf/MI1FHOBDt5bPK9VZ8DvymM= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= -modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= -modernc.org/sqlite v1.18.0/go.mod h1:B9fRWZacNxJBHoCJZQr1R54zhVn3fjfl0aszflrTSxY= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= diff --git a/internal/assets/manager.go b/internal/assets/manager.go index 77ceaf824..e7f84669e 100644 --- a/internal/assets/manager.go +++ b/internal/assets/manager.go @@ -71,6 +71,9 @@ type Manager interface { // From operations.OperationHandler PrepareOperation(ctx context.Context, op *core.Operation) (*core.PreparedOperation, error) RunOperation(ctx context.Context, op *core.PreparedOperation) (outputs fftypes.JSONObject, phase core.OpPhase, err error) + + // Starts the namespace on each of the configured token plugins + Start(ctx context.Context) error } type assetManager struct { @@ -170,6 +173,28 @@ func (am *assetManager) GetTokenConnectors(ctx context.Context) []*core.TokenCon return connectors } +func (am *assetManager) Start(ctx context.Context) error { + f := database.TokenPoolQueryFactory.NewFilter(ctx).And() + pools, _, err := am.database.GetTokenPools(ctx, am.namespace, f) + if err != nil { + return err + } + + for _, plugin := range am.tokens { + activePools := []*core.TokenPool{} + for _, pool := range pools { + if pool.Connector == plugin.ConnectorName() && pool.Active { + activePools = append(activePools, pool) + } + } + err := plugin.StartNamespace(ctx, am.namespace, activePools) + if err != nil { + return err + } + } + return nil +} + func (am *assetManager) getDefaultTokenConnector(ctx context.Context) (string, error) { tokenConnectors := am.GetTokenConnectors(ctx) if len(tokenConnectors) != 1 { diff --git a/internal/assets/manager_test.go b/internal/assets/manager_test.go index b71d7b734..5ccde266b 100644 --- a/internal/assets/manager_test.go +++ b/internal/assets/manager_test.go @@ -18,6 +18,7 @@ package assets import ( "context" "errors" + "fmt" "testing" "time" @@ -160,3 +161,86 @@ func TestGetTokenConnectors(t *testing.T) { assert.Equal(t, 1, len(connectors)) assert.Equal(t, "magic-tokens", connectors[0].Name) } + +func TestStart(t *testing.T) { + coreconfig.Reset() + mdi := &databasemocks.Plugin{} + mdm := &datamocks.Manager{} + mim := &identitymanagermocks.Manager{} + msa := &syncasyncmocks.Bridge{} + mbm := &broadcastmocks.Manager{} + mpm := &privatemessagingmocks.Manager{} + mti := &tokenmocks.Plugin{} + mm := &metricsmocks.Manager{} + mom := &operationmocks.Manager{} + mcm := &contractmocks.Manager{} + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(nil, nil) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) + mdi.On("GetTokenPools", mock.Anything, mock.Anything, mock.Anything).Return([]*core.TokenPool{ + { + Connector: "hot_tokens", + Active: true, + }, + }, nil, nil) + mti.On("StartNamespace", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mti.On("ConnectorName").Return("hot_tokens") + txHelper, _ := txcommon.NewTransactionHelper(context.Background(), "ns1", mdi, mdm, cmi) + am, err := NewAssetManager(context.Background(), "ns1", "blockchain_plugin", mdi, map[string]tokens.Plugin{"magic-tokens": mti}, mim, msa, mbm, mpm, mm, mom, mcm, txHelper, cmi) + assert.NoError(t, err) + err = am.Start(context.Background()) + assert.NoError(t, err) +} + +func TestStartDBError(t *testing.T) { + coreconfig.Reset() + mdi := &databasemocks.Plugin{} + mdm := &datamocks.Manager{} + mim := &identitymanagermocks.Manager{} + msa := &syncasyncmocks.Bridge{} + mbm := &broadcastmocks.Manager{} + mpm := &privatemessagingmocks.Manager{} + mti := &tokenmocks.Plugin{} + mm := &metricsmocks.Manager{} + mom := &operationmocks.Manager{} + mcm := &contractmocks.Manager{} + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(nil, nil) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) + mdi.On("GetTokenPools", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + txHelper, _ := txcommon.NewTransactionHelper(context.Background(), "ns1", mdi, mdm, cmi) + am, err := NewAssetManager(context.Background(), "ns1", "blockchain_plugin", mdi, map[string]tokens.Plugin{"magic-tokens": mti}, mim, msa, mbm, mpm, mm, mom, mcm, txHelper, cmi) + assert.NoError(t, err) + err = am.Start(context.Background()) + assert.Regexp(t, "pop", err) +} + +func TestStartError(t *testing.T) { + coreconfig.Reset() + mdi := &databasemocks.Plugin{} + mdm := &datamocks.Manager{} + mim := &identitymanagermocks.Manager{} + msa := &syncasyncmocks.Bridge{} + mbm := &broadcastmocks.Manager{} + mpm := &privatemessagingmocks.Manager{} + mti := &tokenmocks.Plugin{} + mm := &metricsmocks.Manager{} + mom := &operationmocks.Manager{} + mcm := &contractmocks.Manager{} + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(nil, nil) + mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) + mdi.On("GetTokenPools", mock.Anything, mock.Anything, mock.Anything).Return([]*core.TokenPool{ + { + Connector: "hot_tokens", + Active: true, + }, + }, nil, nil) + mti.On("StartNamespace", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + mti.On("ConnectorName").Return("hot_tokens") + txHelper, _ := txcommon.NewTransactionHelper(context.Background(), "ns1", mdi, mdm, cmi) + am, err := NewAssetManager(context.Background(), "ns1", "blockchain_plugin", mdi, map[string]tokens.Plugin{"magic-tokens": mti}, mim, msa, mbm, mpm, mm, mom, mcm, txHelper, cmi) + assert.NoError(t, err) + err = am.Start(context.Background()) + assert.Regexp(t, "pop", err) +} diff --git a/internal/assets/token_approval.go b/internal/assets/token_approval.go index 406af13e6..ecec79a2e 100644 --- a/internal/assets/token_approval.go +++ b/internal/assets/token_approval.go @@ -63,6 +63,9 @@ func (am *assetManager) NewApproval(approval *core.TokenApprovalInput) syncasync approval: approval, idempotentSubmit: approval.IdempotencyKey != "", } + if approval.Namespace == "" { + approval.Namespace = am.namespace + } sender.setDefaults() return sender } diff --git a/internal/assets/token_transfer.go b/internal/assets/token_transfer.go index 0d26c1c06..11b947b7b 100644 --- a/internal/assets/token_transfer.go +++ b/internal/assets/token_transfer.go @@ -120,6 +120,9 @@ func (am *assetManager) validateTransfer(ctx context.Context, transfer *core.Tok func (am *assetManager) MintTokens(ctx context.Context, transfer *core.TokenTransferInput, waitConfirm bool) (out *core.TokenTransfer, err error) { transfer.Type = core.TokenTransferTypeMint + if transfer.Namespace == "" { + transfer.Namespace = am.namespace + } sender := am.NewTransfer(transfer) if am.metrics.IsMetricsEnabled() { @@ -135,6 +138,9 @@ func (am *assetManager) MintTokens(ctx context.Context, transfer *core.TokenTran func (am *assetManager) BurnTokens(ctx context.Context, transfer *core.TokenTransferInput, waitConfirm bool) (out *core.TokenTransfer, err error) { transfer.Type = core.TokenTransferTypeBurn + if transfer.Namespace == "" { + transfer.Namespace = am.namespace + } sender := am.NewTransfer(transfer) if am.metrics.IsMetricsEnabled() { @@ -150,6 +156,9 @@ func (am *assetManager) BurnTokens(ctx context.Context, transfer *core.TokenTran func (am *assetManager) TransferTokens(ctx context.Context, transfer *core.TokenTransferInput, waitConfirm bool) (out *core.TokenTransfer, err error) { transfer.Type = core.TokenTransferTypeTransfer + if transfer.Namespace == "" { + transfer.Namespace = am.namespace + } sender := am.NewTransfer(transfer) if am.metrics.IsMetricsEnabled() { diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index 7cca630fd..8e63591d0 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -31,7 +31,6 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-common/pkg/retry" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly-signer/pkg/abi" "github.com/hyperledger/firefly-signer/pkg/ffi2abi" @@ -61,24 +60,23 @@ const ( type Ethereum struct { ctx context.Context cancelCtx context.CancelFunc - topic string + pluginTopic string prefixShort string prefixLong string capabilities *blockchain.Capabilities callbacks common.BlockchainCallbacks client *resty.Client streams *streamManager - streamID string - wsconn wsclient.WSClient - closed chan struct{} + streamID map[string]string + wsconn map[string]wsclient.WSClient + wsConfig *wsclient.WSConfig + closed map[string]chan struct{} addressResolveAlways bool addressResolver *addressResolver metrics metrics.Manager ethconnectConf config.Section subs common.FireflySubscriptions cache cache.CInterface - backgroundRetry *retry.Retry - backgroundStart bool } type eventStreamWebsocket struct { @@ -162,7 +160,7 @@ func (e *Ethereum) Init(ctx context.Context, cancelCtx context.CancelFunc, conf return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", ethconnectConf) } - wsConfig, err := wsclient.GenerateConfig(ctx, ethconnectConf) + e.wsConfig, err = wsclient.GenerateConfig(ctx, ethconnectConf) if err == nil { e.client, err = ffresty.New(e.ctx, ethconnectConf) } @@ -171,19 +169,15 @@ func (e *Ethereum) Init(ctx context.Context, cancelCtx context.CancelFunc, conf return err } - e.topic = ethconnectConf.GetString(EthconnectConfigTopic) - if e.topic == "" { + e.pluginTopic = ethconnectConf.GetString(EthconnectConfigTopic) + if e.pluginTopic == "" { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", ethconnectConf) } e.prefixShort = ethconnectConf.GetString(EthconnectPrefixShort) e.prefixLong = ethconnectConf.GetString(EthconnectPrefixLong) - if wsConfig.WSKeyPath == "" { - wsConfig.WSKeyPath = "/ws" - } - e.wsconn, err = wsclient.New(ctx, wsConfig, nil, e.afterConnect) - if err != nil { - return err + if e.wsConfig.WSKeyPath == "" { + e.wsConfig.WSKeyPath = "/ws" } cache, err := cacheManager.GetCache( @@ -199,29 +193,68 @@ func (e *Ethereum) Init(ctx context.Context, cancelCtx context.CancelFunc, conf } e.cache = cache + e.streamID = make(map[string]string) + e.closed = make(map[string]chan struct{}) + e.wsconn = make(map[string]wsclient.WSClient) e.streams = newStreamManager(e.client, e.cache, e.ethconnectConf.GetUint(EthconnectConfigBatchSize), uint(e.ethconnectConf.GetDuration(EthconnectConfigBatchTimeout).Milliseconds())) - e.backgroundStart = e.ethconnectConf.GetBool(EthconnectBackgroundStart) - if e.backgroundStart { - e.backgroundRetry = &retry.Retry{ - InitialDelay: e.ethconnectConf.GetDuration(EthconnectBackgroundStartInitialDelay), - MaximumDelay: e.ethconnectConf.GetDuration(EthconnectBackgroundStartMaxDelay), - Factor: e.ethconnectConf.GetFloat64(EthconnectBackgroundStartFactor), - } + return nil +} - return nil +func (e *Ethereum) getTopic(namespace string) string { + return fmt.Sprintf("%s/%s", e.pluginTopic, namespace) +} + +func (e *Ethereum) StartNamespace(ctx context.Context, namespace string) (err error) { + log.L(e.ctx).Debugf("Starting namespace: %s", namespace) + topic := e.getTopic(namespace) + + e.wsconn[namespace], err = wsclient.New(ctx, e.wsConfig, nil, func(ctx context.Context, w wsclient.WSClient) error { + // Send a subscribe to our topic after each connect/reconnect + b, _ := json.Marshal(ðWSCommandPayload{ + Type: "listen", + Topic: topic, + }) + err := w.Send(ctx, b) + if err == nil { + b, _ = json.Marshal(ðWSCommandPayload{ + Type: "listenreplies", + }) + err = w.Send(ctx, b) + } + return err + }) + if err != nil { + return err + } + // Make sure that our event stream is in place + stream, err := e.streams.ensureEventStream(ctx, topic, e.pluginTopic) + if err != nil { + return err } + log.L(e.ctx).Infof("Event stream: %s (topic=%s)", stream.ID, topic) + e.streamID[namespace] = stream.ID - stream, err := e.streams.ensureEventStream(e.ctx, e.topic) + err = e.wsconn[namespace].Connect() if err != nil { return err } - e.streamID = stream.ID - log.L(e.ctx).Infof("Event stream: %s (topic=%s)", e.streamID, e.topic) + e.closed[namespace] = make(chan struct{}) + + go e.eventLoop(namespace, e.wsconn[namespace], e.closed[namespace]) + + return nil +} - e.closed = make(chan struct{}) - go e.eventLoop() +func (e *Ethereum) StopNamespace(ctx context.Context, namespace string) (err error) { + wsconn, ok := e.wsconn[namespace] + if ok { + wsconn.Close() + } + delete(e.wsconn, namespace) + delete(e.streamID, namespace) + delete(e.closed, namespace) return nil } @@ -234,36 +267,6 @@ func (e *Ethereum) SetOperationHandler(namespace string, handler core.OperationC e.callbacks.SetOperationalHandler(namespace, handler) } -func (e *Ethereum) startBackgroundLoop() { - _ = e.backgroundRetry.Do(e.ctx, fmt.Sprintf("ethereum connector %s", e.Name()), func(attempt int) (retry bool, err error) { - stream, err := e.streams.ensureEventStream(e.ctx, e.topic) - if err != nil { - return true, err - } - - e.streamID = stream.ID - log.L(e.ctx).Infof("Event stream: %s (topic=%s)", e.streamID, e.topic) - err = e.wsconn.Connect() - if err != nil { - return true, err - } - - e.closed = make(chan struct{}) - go e.eventLoop() - - return false, nil - }) -} - -func (e *Ethereum) Start() (err error) { - if e.backgroundStart { - go e.startBackgroundLoop() - return nil - } - - return e.wsconn.Connect() -} - func (e *Ethereum) Capabilities() *blockchain.Capabilities { return e.capabilities } @@ -279,7 +282,12 @@ func (e *Ethereum) AddFireflySubscription(ctx context.Context, namespace *core.N return "", err } - sub, err := e.streams.ensureFireFlySubscription(ctx, namespace.Name, version, ethLocation.Address, contract.FirstEvent, e.streamID, batchPinEventABI) + streamID, ok := e.streamID[namespace.Name] + if !ok { + return "", i18n.NewError(ctx, coremsgs.MsgInternalServerError, "eventstream ID not found") + } + sub, err := e.streams.ensureFireFlySubscription(ctx, namespace.Name, version, ethLocation.Address, contract.FirstEvent, streamID, batchPinEventABI) + if err != nil { return "", err } @@ -295,22 +303,6 @@ func (e *Ethereum) RemoveFireflySubscription(ctx context.Context, subID string) e.subs.RemoveSubscription(ctx, subID) } -func (e *Ethereum) afterConnect(ctx context.Context, w wsclient.WSClient) error { - // Send a subscribe to our topic after each connect/reconnect - b, _ := json.Marshal(ðWSCommandPayload{ - Type: "listen", - Topic: e.topic, - }) - err := w.Send(ctx, b) - if err == nil { - b, _ = json.Marshal(ðWSCommandPayload{ - Type: "listenreplies", - }) - err = w.Send(ctx, b) - } - return err -} - func ethHexFormatB32(b *fftypes.Bytes32) string { if b == nil { return "0x0000000000000000000000000000000000000000000000000000000000000000" @@ -460,17 +452,19 @@ func (e *Ethereum) handleMessageBatch(ctx context.Context, batchID int64, messag return e.callbacks.DispatchBlockchainEvents(ctx, events) } -func (e *Ethereum) eventLoop() { - defer e.wsconn.Close() - defer close(e.closed) - l := log.L(e.ctx).WithField("role", "event-loop") +func (e *Ethereum) eventLoop(namespace string, wsconn wsclient.WSClient, closed chan struct{}) { + topic := e.getTopic(namespace) + defer wsconn.Close() + defer close(closed) + l := log.L(e.ctx).WithField("role", "event-loop").WithField("namespace", namespace) ctx := log.WithLogger(e.ctx, l) + log.L(ctx).Debugf("Starting event loop for namespace '%s'", namespace) for { select { case <-ctx.Done(): l.Debugf("Event loop exiting (context cancelled)") return - case msgBytes, ok := <-e.wsconn.Receive(): + case msgBytes, ok := <-wsconn.Receive(): if !ok { l.Debugf("Event loop exiting (receive channel closed). Terminating server!") e.cancelCtx() @@ -489,9 +483,9 @@ func (e *Ethereum) eventLoop() { if err == nil { ack, _ := json.Marshal(ðWSCommandPayload{ Type: "ack", - Topic: e.topic, + Topic: topic, }) - err = e.wsconn.Send(ctx, ack) + err = wsconn.Send(ctx, ack) } case map[string]interface{}: isBatch := false @@ -502,7 +496,7 @@ func (e *Ethereum) eventLoop() { err = e.handleMessageBatch(ctx, (int64)(batchNumber), events) // Errors processing messages are converted into nacks ackOrNack := ðWSCommandPayload{ - Topic: e.topic, + Topic: topic, BatchNumber: int64(batchNumber), } if err == nil { @@ -513,7 +507,7 @@ func (e *Ethereum) eventLoop() { ackOrNack.Message = err.Error() } b, _ := json.Marshal(&ackOrNack) - err = e.wsconn.Send(ctx, b) + err = wsconn.Send(ctx, b) } } if !isBatch { @@ -877,6 +871,7 @@ func (e *Ethereum) encodeContractLocation(ctx context.Context, location *Locatio func (e *Ethereum) AddContractListener(ctx context.Context, listener *core.ContractListener) (err error) { var location *Location + namespace := listener.Namespace if listener.Location != nil { location, err = e.parseContractLocation(ctx, listener.Location) if err != nil { @@ -893,7 +888,7 @@ func (e *Ethereum) AddContractListener(ctx context.Context, listener *core.Contr if listener.Options != nil { firstEvent = listener.Options.FirstEvent } - result, err := e.streams.createSubscription(ctx, location, e.streamID, subName, firstEvent, abi) + result, err := e.streams.createSubscription(ctx, location, e.streamID[namespace], subName, firstEvent, abi) if err != nil { return err } @@ -905,9 +900,10 @@ func (e *Ethereum) DeleteContractListener(ctx context.Context, subscription *cor return e.streams.deleteSubscription(ctx, subscription.BackendID, okNotFound) } -func (e *Ethereum) GetContractListenerStatus(ctx context.Context, subID string, okNotFound bool) (found bool, status interface{}, err error) { +func (e *Ethereum) GetContractListenerStatus(ctx context.Context, namespace, subID string, okNotFound bool) (found bool, status interface{}, err error) { + esID := e.streamID[namespace] sub, err := e.streams.getSubscription(ctx, subID, okNotFound) - if err != nil || sub == nil { + if err != nil || sub == nil || sub.Stream != esID { return false, nil, err } diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 76b7214d1..c2cae7a23 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -32,7 +32,6 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftls" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-common/pkg/retry" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly/internal/blockchain/common" "github.com/hyperledger/firefly/internal/cache" @@ -120,30 +119,38 @@ func resetConf(e *Ethereum) { func newTestEthereum() (*Ethereum, func()) { ctx, cancel := context.WithCancel(context.Background()) - wsm := &wsmocks.WSClient{} mm := &metricsmocks.Manager{} mm.On("IsMetricsEnabled").Return(true) mm.On("BlockchainTransaction", mock.Anything, mock.Anything).Return(nil) mm.On("BlockchainContractDeployment", mock.Anything, mock.Anything).Return(nil) mm.On("BlockchainQuery", mock.Anything, mock.Anything).Return(nil) + r := resty.New().SetBaseURL("http://localhost:12345") e := &Ethereum{ ctx: ctx, cancelCtx: cancel, - client: resty.New().SetBaseURL("http://localhost:12345"), - topic: "topic1", + client: r, + pluginTopic: "topic1", prefixShort: defaultPrefixShort, prefixLong: defaultPrefixLong, - wsconn: wsm, + streamID: make(map[string]string), + wsconn: make(map[string]wsclient.WSClient), + closed: make(map[string]chan struct{}), + wsConfig: &wsclient.WSConfig{}, metrics: mm, cache: cache.NewUmanagedCache(ctx, 100, 5*time.Minute), callbacks: common.NewBlockchainCallbacks(), subs: common.NewFireflySubscriptions(), + streams: &streamManager{ + client: r, + }, } return e, func() { cancel() if e.closed != nil { // We've init'd, wait to close - <-e.closed + for _, cls := range e.closed { + <-cls + } } } } @@ -255,20 +262,20 @@ func TestInitAndStartWithEthConnect(t *testing.T) { assert.NoError(t, err) + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + assert.Equal(t, 2, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) + assert.Equal(t, "es12345", e.streamID["ns1"]) assert.NotNil(t, e.Capabilities()) - err = e.Start() - assert.NoError(t, err) - startupMessage := <-toServer - assert.Equal(t, `{"type":"listen","topic":"topic1"}`, startupMessage) + assert.Equal(t, `{"type":"listen","topic":"topic1/ns1"}`, startupMessage) startupMessage = <-toServer assert.Equal(t, `{"type":"listenreplies"}`, startupMessage) fromServer <- `[]` // empty batch, will be ignored, but acked reply := <-toServer - assert.Equal(t, `{"type":"ack","topic":"topic1"}`, reply) + assert.Equal(t, `{"type":"ack","topic":"topic1/ns1"}`, reply) // Bad data will be ignored fromServer <- `!json` @@ -311,6 +318,9 @@ func TestInitAndStartWithFFTM(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + msb := &blockchaincommonmocks.FireflySubscriptions{} e.subs = msb msb.On("GetSubscription", mock.Anything).Return(&common.SubscriptionInfo{ @@ -323,41 +333,35 @@ func TestInitAndStartWithFFTM(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 2, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) + assert.Equal(t, "es12345", e.streamID["ns1"]) assert.NotNil(t, e.Capabilities()) - err = e.Start() - assert.NoError(t, err) - startupMessage := <-toServer - assert.Equal(t, `{"type":"listen","topic":"topic1"}`, startupMessage) + assert.Equal(t, `{"type":"listen","topic":"topic1/ns1"}`, startupMessage) startupMessage = <-toServer assert.Equal(t, `{"type":"listenreplies"}`, startupMessage) fromServer <- `{"bad":"receipt"}` // bad receipt that cannot be handled - will be swallowed fromServer <- `{"batchNumber":12345,"events":[]}` // empty batch, will be ignored, but acked reply := <-toServer - assert.Equal(t, `{"type":"ack","topic":"topic1","batchNumber":12345}`, reply) + assert.Equal(t, `{"type":"ack","topic":"topic1/ns1","batchNumber":12345}`, reply) fromServer <- `{"batchNumber":12345,"events":[{ "bad":"batch" }]}` // empty batch, will be ignored, but nack'd as it is invalid reply = <-toServer - assert.Regexp(t, `{"type":"error","topic":"topic1","batchNumber":12345,"message":"FF10141.*"}`, reply) + assert.Regexp(t, `{"type":"error","topic":"topic1/ns1","batchNumber":12345,"message":"FF10141.*"}`, reply) // Bad data will be ignored fromServer <- `!json` fromServer <- `{"not": "a reply"}` fromServer <- `42` - } -func TestBackgroundStart(t *testing.T) { - - log.SetLevel("trace") +func TestStartStopNamespace(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - toServer, fromServer, wsURL, done := wsclient.NewTestWSServer(nil) + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) defer done() mockedClient := &http.Client{} @@ -378,49 +382,27 @@ func TestBackgroundStart(t *testing.T) { utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - utEthconnectConf.Set(EthconnectBackgroundStart, true) - utFFTMConf.Set(ffresty.HTTPConfigURL, "http://ethc.example.com:12345") + utFFTMConf.Set(ffresty.HTTPConfigURL, httpURL) cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) - assert.Equal(t, "ethereum", e.Name()) - assert.Equal(t, core.VerifierTypeEthAddress, e.VerifierType()) - + err = e.StartNamespace(e.ctx, "ns1") assert.NoError(t, err) - assert.NotNil(t, e.Capabilities()) + <-toServer - err = e.Start() + err = e.StopNamespace(e.ctx, "ns1") assert.NoError(t, err) - - assert.Eventually(t, func() bool { return httpmock.GetTotalCallCount() == 2 }, time.Second*5, time.Microsecond) - assert.Eventually(t, func() bool { return e.streamID == "es12345" }, time.Second*5, time.Microsecond) - - startupMessage := <-toServer - assert.Equal(t, `{"type":"listen","topic":"topic1"}`, startupMessage) - startupMessage = <-toServer - assert.Equal(t, `{"type":"listenreplies"}`, startupMessage) - fromServer <- `[]` // empty batch, will be ignored, but acked - reply := <-toServer - assert.Equal(t, `{"type":"ack","topic":"topic1"}`, reply) - - // Bad data will be ignored - fromServer <- `!json` - fromServer <- `{"not": "a reply"}` - fromServer <- `42` - } -func TestBackgroundStartFail(t *testing.T) { - - log.SetLevel("trace") +func TestStartStopNamespaceOldEventstream(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - _, _, wsURL, done := wsclient.NewTestWSServer(nil) + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) defer done() mockedClient := &http.Client{} @@ -432,46 +414,39 @@ func TestBackgroundStartFail(t *testing.T) { httpURL := u.String() httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), - httpmock.NewJsonResponderOrPanic(500, "Failed to get eventstreams")) + httpmock.NewJsonResponderOrPanic(200, []eventStream{ + { + ID: "es12345", + Name: "topic1", + }, + })) + httpmock.RegisterResponder("DELETE", fmt.Sprintf("%s/eventstreams/es12345", httpURL), + httpmock.NewJsonResponderOrPanic(204, "")) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - utEthconnectConf.Set(EthconnectBackgroundStart, true) - utFFTMConf.Set(ffresty.HTTPConfigURL, "http://ethc.example.com:12345") + utFFTMConf.Set(ffresty.HTTPConfigURL, httpURL) cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) - assert.Equal(t, "ethereum", e.Name()) - assert.Equal(t, core.VerifierTypeEthAddress, e.VerifierType()) - - assert.NoError(t, err) - - err = e.Start() + err = e.StartNamespace(e.ctx, "ns1") assert.NoError(t, err) - capturedErr := make(chan error) - e.backgroundRetry = &retry.Retry{ - ErrCallback: func(err error) { - capturedErr <- err - }, - } + <-toServer - err = e.Start() + err = e.StopNamespace(e.ctx, "ns1") assert.NoError(t, err) - - err = <-capturedErr - assert.Regexp(t, "FF10111", err) } -func TestBackgroundStartWSFail(t *testing.T) { - - log.SetLevel("trace") +func TestEnsureEventStreamDeleteFail(t *testing.T) { e, cancel := newTestEthereum() defer cancel() @@ -479,52 +454,64 @@ func TestBackgroundStartWSFail(t *testing.T) { httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - u, _ := url.Parse("http://localhost:12345") - u.Scheme = "http" - httpURL := u.String() + httpURL := "http://localhost:12345" httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), - httpmock.NewJsonResponderOrPanic(200, []eventStream{})) - httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL), - httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.NewJsonResponderOrPanic(200, []eventStream{ + { + ID: "es12345", + Name: "topic1", + }, + })) + httpmock.RegisterResponder("DELETE", fmt.Sprintf("%s/eventstreams/es12345", httpURL), + httpmock.NewJsonResponderOrPanic(500, "pop")) resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") - utEthconnectConf.Set(EthconnectBackgroundStart, true) - utEthconnectConf.Set(wsclient.WSConfigKeyInitialConnectAttempts, 1) - utFFTMConf.Set(ffresty.HTTPConfigURL, "http://ethc.example.com:12345") + utFFTMConf.Set(ffresty.HTTPConfigURL, httpURL) cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) - originalContext := e.ctx - err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) - cmi.AssertCalled(t, "GetCache", cache.NewCacheConfig( - originalContext, - coreconfig.CacheBlockchainLimit, - coreconfig.CacheBlockchainTTL, - "", - )) + err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) - capturedErr := make(chan error) - e.backgroundRetry = &retry.Retry{ - ErrCallback: func(err error) { - capturedErr <- err - }, - } + _, err = e.streams.ensureEventStream(context.Background(), "topic1/ns1", "topic1") + assert.Regexp(t, "pop", err) +} + +func TestDeleteStreamOKNotFound(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpURL := "http://localhost:12345" - err = e.Start() + httpmock.RegisterResponder("DELETE", fmt.Sprintf("%s/eventstreams/es12345", httpURL), + httpmock.NewJsonResponderOrPanic(404, "pop")) + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utFFTMConf.Set(ffresty.HTTPConfigURL, httpURL) + + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) + err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) - err = <-capturedErr - assert.Regexp(t, "FF00148", err) + err = e.streams.deleteEventStream(context.Background(), "es12345", true) + assert.NoError(t, err) } -func TestWSInitFail(t *testing.T) { - +func TestStartNamespaceWSCreateFail(t *testing.T) { e, cancel := newTestEthereum() defer cancel() @@ -536,8 +523,43 @@ func TestWSInitFail(t *testing.T) { cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) + assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") assert.Regexp(t, "FF00149", err) +} +func TestStartNamespaceWSConnectFail(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpURL := "http://fftm.example.com:12345" + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/ws", httpURL), + httpmock.NewJsonResponderOrPanic(500, "{}")) + + resetConf(e) + utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utFFTMConf.Set(ffresty.HTTPConfigURL, httpURL) + + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) + err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) + assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") + assert.Regexp(t, "FF00148", err) } func TestEthCacheInitFail(t *testing.T) { @@ -629,7 +651,6 @@ func TestInitOldInstancePathContracts(t *testing.T) { cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) - assert.NoError(t, err) } func TestInitOldInstancePathInstances(t *testing.T) { @@ -651,7 +672,7 @@ func TestInitOldInstancePathInstances(t *testing.T) { func(req *http.Request) (*http.Response, error) { var body map[string]interface{} json.NewDecoder(req.Body).Decode(&body) - assert.Equal(t, "es12345", body["stream"]) + assert.Equal(t, "es12345/ns1", body["stream"]) return httpmock.NewJsonResponderOrPanic(200, subscription{ID: "sub12345"})(req) }) httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1)) @@ -678,11 +699,6 @@ func TestInitNewConfig(t *testing.T) { httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{})) - httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) - resetConf(e) utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) @@ -691,7 +707,6 @@ func TestInitNewConfig(t *testing.T) { cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) - assert.Equal(t, 2, httpmock.GetTotalCallCount()) assert.NoError(t, err) } @@ -773,8 +788,10 @@ func TestStreamQueryError(t *testing.T) { cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) - assert.Regexp(t, "FF10111.*pop", err) + assert.NoError(t, err) + err = e.StartNamespace(e.ctx, "ns1") + assert.Regexp(t, "FF10111.*pop", err) } func TestStreamCreateError(t *testing.T) { @@ -801,8 +818,10 @@ func TestStreamCreateError(t *testing.T) { cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) - assert.Regexp(t, "FF10111.*pop", err) + assert.NoError(t, err) + err = e.StartNamespace(e.ctx, "ns1") + assert.Regexp(t, "FF10111.*pop", err) } func TestStreamUpdateError(t *testing.T) { @@ -815,7 +834,7 @@ func TestStreamUpdateError(t *testing.T) { defer httpmock.DeactivateAndReset() httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345", httpmock.NewStringResponder(500, `pop`)) @@ -829,6 +848,8 @@ func TestStreamUpdateError(t *testing.T) { cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) + assert.NoError(t, err) + err = e.StartNamespace(e.ctx, "ns1") assert.Regexp(t, "FF10111.*pop", err) } @@ -836,27 +857,36 @@ func TestInitAllExistingStreams(t *testing.T) { e, cancel := newTestEthereum() defer cancel() + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() + mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, []subscription{ {ID: "sub12345", Stream: "es12345", Name: "ns1_BatchPin_3078373143373635" /* this is the subname for our combo of instance path and BatchPin */}, })) - httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345", - httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", Name: "topic1"})) - httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 2)) - httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + + httpmock.RegisterResponder("PATCH", fmt.Sprintf("%s/eventstreams/es12345", httpURL), + httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", Name: "topic1/ns1"})) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/", httpURL), mockNetworkVersion(t, 2)) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, subscription{})) resetConf(e) - utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") + utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "/instances/0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + utFFTMConf.Set(ffresty.HTTPConfigURL, "http://fftm.example.com:12345") location := fftypes.JSONAnyPtr(fftypes.JSONObject{ "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", @@ -870,36 +900,48 @@ func TestInitAllExistingStreams(t *testing.T) { cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + + <-toServer + ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) assert.Equal(t, 4, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) + assert.Equal(t, "es12345", e.streamID["ns1"]) } func TestInitAllExistingStreamsV1(t *testing.T) { e, cancel := newTestEthereum() defer cancel() + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() + mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, []subscription{ {ID: "sub12345", Stream: "es12345", Name: "BatchPin_3078373143373635" /* this is the subname for our combo of instance path and BatchPin */}, })) - httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345", - httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", Name: "topic1"})) - httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1)) - httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.RegisterResponder("PATCH", fmt.Sprintf("%s/eventstreams/es12345", httpURL), + httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", Name: "topic1/ns1"})) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/", httpURL), mockNetworkVersion(t, 1)) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, subscription{})) resetConf(e) - utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") @@ -916,36 +958,48 @@ func TestInitAllExistingStreamsV1(t *testing.T) { cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + + <-toServer + ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) assert.Equal(t, 4, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) + assert.Equal(t, "es12345", e.streamID["ns1"]) } func TestInitAllExistingStreamsOld(t *testing.T) { e, cancel := newTestEthereum() defer cancel() + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() + mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, []subscription{ {ID: "sub12345", Stream: "es12345", Name: "BatchPin"}, })) - httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345", - httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", Name: "topic1"})) - httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1)) - httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.RegisterResponder("PATCH", fmt.Sprintf("%s/eventstreams/es12345", httpURL), + httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", Name: "topic1/ns1"})) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/", httpURL), mockNetworkVersion(t, 1)) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, subscription{})) resetConf(e) - utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") @@ -962,36 +1016,48 @@ func TestInitAllExistingStreamsOld(t *testing.T) { cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + + <-toServer + ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) assert.Equal(t, 4, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) + assert.Equal(t, "es12345", e.streamID["ns1"]) } func TestInitAllExistingStreamsInvalidName(t *testing.T) { e, cancel := newTestEthereum() defer cancel() + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() + mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, []subscription{ {ID: "sub12345", Stream: "es12345", Name: "BatchPin_3078373143373635" /* this is the subname for our combo of instance path and BatchPin */}, })) - httpmock.RegisterResponder("PATCH", "http://localhost:12345/eventstreams/es12345", - httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", Name: "topic1"})) - httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 2)) - httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.RegisterResponder("PATCH", fmt.Sprintf("%s/eventstreams/es12345", httpURL), + httpmock.NewJsonResponderOrPanic(200, &eventStream{ID: "es12345", Name: "topic1/ns1"})) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/", httpURL), mockNetworkVersion(t, 2)) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, subscription{})) resetConf(e) - utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utEthconnectConf.Set(EthconnectConfigInstanceDeprecated, "0x71C7656EC7ab88b098defB751B7401B5f6d8976F") utEthconnectConf.Set(EthconnectConfigTopic, "topic1") @@ -1008,6 +1074,11 @@ func TestInitAllExistingStreamsInvalidName(t *testing.T) { cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + + <-toServer ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.Regexp(t, "FF10416", err) @@ -1670,11 +1741,12 @@ func TestEventLoopContextCancelled(t *testing.T) { e, cancel := newTestEthereum() cancel() r := make(<-chan []byte) - wsm := e.wsconn.(*wsmocks.WSClient) + wsm := &wsmocks.WSClient{} + e.wsconn["ns1"] = wsm wsm.On("Receive").Return(r) wsm.On("Close").Return() - e.closed = make(chan struct{}) - e.eventLoop() // we're simply looking for it exiting + e.closed["ns1"] = make(chan struct{}) + e.eventLoop("ns1", wsm, e.closed["ns1"]) // we're simply looking for it exiting wsm.AssertExpectations(t) } @@ -1682,12 +1754,13 @@ func TestEventLoopReceiveClosed(t *testing.T) { e, cancel := newTestEthereum() defer cancel() r := make(chan []byte) - wsm := e.wsconn.(*wsmocks.WSClient) + wsm := &wsmocks.WSClient{} + e.wsconn["ns1"] = wsm close(r) wsm.On("Receive").Return((<-chan []byte)(r)) wsm.On("Close").Return() - e.closed = make(chan struct{}) - e.eventLoop() // we're simply looking for it exiting + e.closed["ns1"] = make(chan struct{}) + e.eventLoop("ns1", wsm, e.closed["ns1"]) // we're simply looking for it exiting wsm.AssertExpectations(t) } @@ -1696,15 +1769,16 @@ func TestEventLoopSendClosed(t *testing.T) { s := make(chan []byte, 1) s <- []byte(`[]`) r := make(chan []byte) - wsm := e.wsconn.(*wsmocks.WSClient) + wsm := &wsmocks.WSClient{} + e.wsconn["ns1"] = wsm wsm.On("Receive").Return((<-chan []byte)(s)) wsm.On("Send", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { go cancel() close(r) }).Return(fmt.Errorf("pop")) wsm.On("Close").Return() - e.closed = make(chan struct{}) - e.eventLoop() // we're simply looking for it exiting + e.closed["ns1"] = make(chan struct{}) + e.eventLoop("ns1", wsm, e.closed["ns1"]) // we're simply looking for it exiting wsm.AssertExpectations(t) } @@ -1712,10 +1786,10 @@ func TestHandleReceiptTXSuccess(t *testing.T) { em := &coremocks.OperationCallbacks{} wsm := &wsmocks.WSClient{} e := &Ethereum{ - ctx: context.Background(), - topic: "topic1", - callbacks: common.NewBlockchainCallbacks(), - wsconn: wsm, + ctx: context.Background(), + pluginTopic: "topic1", + callbacks: common.NewBlockchainCallbacks(), + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, } e.SetOperationHandler("ns1", em) @@ -1763,10 +1837,10 @@ func TestHandleReceiptTXUpdateEVMConnect(t *testing.T) { em := &coremocks.OperationCallbacks{} wsm := &wsmocks.WSClient{} e := &Ethereum{ - ctx: context.Background(), - topic: "topic1", - callbacks: common.NewBlockchainCallbacks(), - wsconn: wsm, + ctx: context.Background(), + pluginTopic: "topic1", + callbacks: common.NewBlockchainCallbacks(), + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, } e.SetOperationHandler("ns1", em) @@ -1850,8 +1924,9 @@ func TestHandleBadPayloadsAndThenReceiptFailure(t *testing.T) { e, cancel := newTestEthereum() defer cancel() r := make(chan []byte) - wsm := e.wsconn.(*wsmocks.WSClient) - e.closed = make(chan struct{}) + wsm := &wsmocks.WSClient{} + e.wsconn["ns1"] = wsm + e.closed["ns1"] = make(chan struct{}) wsm.On("Receive").Return((<-chan []byte)(r)) wsm.On("Close").Return() @@ -1884,7 +1959,7 @@ func TestHandleBadPayloadsAndThenReceiptFailure(t *testing.T) { close(done) } - go e.eventLoop() + go e.eventLoop("ns1", wsm, e.closed["ns1"]) r <- []byte(`!badjson`) // ignored bad json r <- []byte(`"not an object"`) // ignored wrong type r <- data.Bytes() @@ -1896,9 +1971,9 @@ func TestHandleBadPayloadsAndThenReceiptFailure(t *testing.T) { func TestHandleMsgBatchBadData(t *testing.T) { wsm := &wsmocks.WSClient{} e := &Ethereum{ - ctx: context.Background(), - topic: "topic1", - wsconn: wsm, + ctx: context.Background(), + pluginTopic: "topic1", + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, } var reply common.BlockchainReceiptNotification @@ -1917,7 +1992,7 @@ func TestAddSubscription(t *testing.T) { defer cancel() httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -1955,7 +2030,7 @@ func TestAddSubscriptionWithoutLocation(t *testing.T) { defer cancel() httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -1990,7 +2065,7 @@ func TestAddSubscriptionBadParamDetails(t *testing.T) { defer cancel() httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -2026,7 +2101,7 @@ func TestAddSubscriptionBadLocation(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -2047,7 +2122,7 @@ func TestAddSubscriptionFail(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -2077,7 +2152,7 @@ func TestDeleteSubscription(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -2100,7 +2175,7 @@ func TestDeleteSubscriptionFail(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -2123,7 +2198,7 @@ func TestDeleteSubscriptionNotFound(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -3971,6 +4046,7 @@ func TestAddAndRemoveFireflySubscription(t *testing.T) { FirstEvent: "newest", } + e.streamID["ns1"] = "es12345" ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} subID, err := e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) @@ -4018,12 +4094,56 @@ func TestAddFireflySubscriptionV1(t *testing.T) { FirstEvent: "newest", } + e.streamID["ns1"] = "es12345" ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) assert.NotNil(t, e.subs.GetSubscription("sub1")) } +func TestAddFireflySubscriptionEventstreamFail(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + resetConf(e) + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{})) + httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, subscription{ + ID: "sub1", + })) + httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(t, 1)) + + utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utEthconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utEthconnectConf.Set(EthconnectConfigTopic, "topic1") + + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) + err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) + // assert.NoError(t, err) + + location := fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()) + contract := &blockchain.MultipartyContract{ + Location: location, + FirstEvent: "newest", + } + + ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} + _, err = e.AddFireflySubscription(e.ctx, ns, contract) + assert.Regexp(t, "FF10465", err) +} + func TestAddFireflySubscriptionQuerySubsFail(t *testing.T) { e, cancel := newTestEthereum() defer cancel() @@ -4060,6 +4180,7 @@ func TestAddFireflySubscriptionQuerySubsFail(t *testing.T) { FirstEvent: "oldest", } + e.streamID["ns1"] = "es12345" ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.Regexp(t, "FF10111", err) @@ -4143,6 +4264,7 @@ func TestAddFireflySubscriptionGetVersionError(t *testing.T) { FirstEvent: "oldest", } + e.streamID["ns1"] = "es12345" ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.Regexp(t, "FF10111", err) @@ -4186,7 +4308,8 @@ func TestGetContractListenerStatus(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) - found, status, err := e.GetContractListenerStatus(context.Background(), "sub1", true) + e.streamID["ns1"] = "es12345" + found, status, err := e.GetContractListenerStatus(context.Background(), "ns1", "sub1", true) assert.NotNil(t, status) assert.NoError(t, err) assert.True(t, found) @@ -4219,7 +4342,8 @@ func TestGetContractListenerStatusGetSubFail(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) - found, status, err := e.GetContractListenerStatus(context.Background(), "sub1", true) + e.streamID["ns1"] = "es12345" + found, status, err := e.GetContractListenerStatus(context.Background(), "ns1", "sub1", true) assert.Nil(t, status) assert.Regexp(t, "FF10111", err) assert.False(t, found) @@ -4252,7 +4376,8 @@ func TestGetContractListenerStatusGetSubNotFound(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) assert.NoError(t, err) - found, status, err := e.GetContractListenerStatus(context.Background(), "sub1", true) + e.streamID["ns1"] = "es12345" + found, status, err := e.GetContractListenerStatus(context.Background(), "ns1", "sub1", true) assert.Nil(t, status) assert.Nil(t, err) assert.False(t, found) diff --git a/internal/blockchain/ethereum/eventstream.go b/internal/blockchain/ethereum/eventstream.go index 8b1d505ab..498f3117d 100644 --- a/internal/blockchain/ethereum/eventstream.go +++ b/internal/blockchain/ethereum/eventstream.go @@ -127,7 +127,7 @@ func (s *streamManager) updateEventStream(ctx context.Context, topic string, bat return stream, nil } -func (s *streamManager) ensureEventStream(ctx context.Context, topic string) (*eventStream, error) { +func (s *streamManager) ensureEventStream(ctx context.Context, topic, pluginTopic string) (*eventStream, error) { existingStreams, err := s.getEventStreams(ctx) if err != nil { return nil, err @@ -140,10 +140,29 @@ func (s *streamManager) ensureEventStream(ctx context.Context, topic string) (*e } return stream, nil } + if stream.Name == pluginTopic { + // We have an old event stream that needs to get deleted + if err := s.deleteEventStream(ctx, stream.ID, false); err != nil { + return nil, err + } + } } return s.createEventStream(ctx, topic) } +func (s *streamManager) deleteEventStream(ctx context.Context, esID string, okNotFound bool) error { + res, err := s.client.R(). + SetContext(ctx). + Delete("/eventstreams/" + esID) + if err != nil || !res.IsSuccess() { + if okNotFound && res.StatusCode() == 404 { + return nil + } + return ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgEthConnectorRESTErr) + } + return nil +} + func (s *streamManager) getSubscriptions(ctx context.Context) (subs []*subscription, err error) { res, err := s.client.R(). SetContext(ctx). diff --git a/internal/blockchain/fabric/eventstream.go b/internal/blockchain/fabric/eventstream.go index 3db62c3ee..49c2c2ec5 100644 --- a/internal/blockchain/fabric/eventstream.go +++ b/internal/blockchain/fabric/eventstream.go @@ -111,7 +111,7 @@ func (s *streamManager) createEventStream(ctx context.Context, topic string) (*e return stream, nil } -func (s *streamManager) ensureEventStream(ctx context.Context, topic string) (*eventStream, error) { +func (s *streamManager) ensureEventStream(ctx context.Context, topic, pluginTopic string) (*eventStream, error) { existingStreams, err := s.getEventStreams(ctx) if err != nil { return nil, err @@ -120,10 +120,29 @@ func (s *streamManager) ensureEventStream(ctx context.Context, topic string) (*e if stream.Name == topic { return stream, nil } + if stream.Name == pluginTopic { + // We have an old event stream that needs to get deleted + if err := s.deleteEventStream(ctx, stream.ID, false); err != nil { + return nil, err + } + } } return s.createEventStream(ctx, topic) } +func (s *streamManager) deleteEventStream(ctx context.Context, esID string, okNotFound bool) error { + res, err := s.client.R(). + SetContext(ctx). + Delete("/eventstreams/" + esID) + if err != nil || !res.IsSuccess() { + if okNotFound && res.StatusCode() == 404 { + return nil + } + return ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgFabconnectRESTErr) + } + return nil +} + func (s *streamManager) getSubscriptions(ctx context.Context) (subs []*subscription, err error) { res, err := s.client.R(). SetContext(ctx). diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index 03b3fca8e..89be2a2fc 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -31,7 +31,6 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-common/pkg/retry" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly/internal/blockchain/common" "github.com/hyperledger/firefly/internal/cache" @@ -47,27 +46,26 @@ const ( ) type Fabric struct { - ctx context.Context - cancelCtx context.CancelFunc - topic string - defaultChannel string - signer string - prefixShort string - prefixLong string - capabilities *blockchain.Capabilities - callbacks common.BlockchainCallbacks - client *resty.Client - streams *streamManager - streamID string - idCache map[string]*fabIdentity - wsconn wsclient.WSClient - closed chan struct{} - metrics metrics.Manager - fabconnectConf config.Section - subs common.FireflySubscriptions - cache cache.CInterface - backgroundRetry *retry.Retry - backgroundStart bool + ctx context.Context + cancelCtx context.CancelFunc + pluginTopic string + defaultChannel string + signer string + prefixShort string + prefixLong string + capabilities *blockchain.Capabilities + callbacks common.BlockchainCallbacks + client *resty.Client + streams *streamManager + streamID map[string]string + idCache map[string]*fabIdentity + wsconn map[string]wsclient.WSClient + wsConfig *wsclient.WSConfig + closed map[string]chan struct{} + metrics metrics.Manager + fabconnectConf config.Section + subs common.FireflySubscriptions + cache cache.CInterface } type eventStreamWebsocket struct { @@ -205,7 +203,7 @@ func (f *Fabric) Init(ctx context.Context, cancelCtx context.CancelFunc, conf co return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "blockchain.fabric.fabconnect") } - wsConfig, err := wsclient.GenerateConfig(ctx, fabconnectConf) + f.wsConfig, err = wsclient.GenerateConfig(ctx, fabconnectConf) if err == nil { f.client, err = ffresty.New(f.ctx, fabconnectConf) } @@ -217,21 +215,17 @@ func (f *Fabric) Init(ctx context.Context, cancelCtx context.CancelFunc, conf co f.defaultChannel = fabconnectConf.GetString(FabconnectConfigDefaultChannel) // the org identity is guaranteed to be configured by the core f.signer = fabconnectConf.GetString(FabconnectConfigSigner) - f.topic = fabconnectConf.GetString(FabconnectConfigTopic) - if f.topic == "" { + f.pluginTopic = fabconnectConf.GetString(FabconnectConfigTopic) + if f.pluginTopic == "" { return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "topic", "blockchain.fabric.fabconnect") } f.prefixShort = fabconnectConf.GetString(FabconnectPrefixShort) f.prefixLong = fabconnectConf.GetString(FabconnectPrefixLong) - if wsConfig.WSKeyPath == "" { - wsConfig.WSKeyPath = "/ws" + if f.wsConfig.WSKeyPath == "" { + f.wsConfig.WSKeyPath = "/ws" } - f.wsconn, err = wsclient.New(f.ctx, wsConfig, nil, f.afterConnect) - if err != nil { - return err - } cache, err := cacheManager.GetCache( cache.NewCacheConfig( ctx, @@ -245,28 +239,68 @@ func (f *Fabric) Init(ctx context.Context, cancelCtx context.CancelFunc, conf co } f.cache = cache + f.streamID = make(map[string]string) + f.closed = make(map[string]chan struct{}) + f.wsconn = make(map[string]wsclient.WSClient) f.streams = newStreamManager(f.client, f.signer, f.cache, f.fabconnectConf.GetUint(FabconnectConfigBatchSize), uint(f.fabconnectConf.GetDuration(FabconnectConfigBatchTimeout).Milliseconds())) - f.backgroundStart = f.fabconnectConf.GetBool(FabconnectBackgroundStart) + return nil +} + +func (f *Fabric) getTopic(namespace string) string { + return fmt.Sprintf("%s/%s", f.pluginTopic, namespace) +} + +func (f *Fabric) StartNamespace(ctx context.Context, namespace string) (err error) { + log.L(f.ctx).Debugf("Starting namespace: %s", namespace) + topic := f.getTopic(namespace) - if f.backgroundStart { - f.backgroundRetry = &retry.Retry{ - InitialDelay: f.fabconnectConf.GetDuration(FabconnectBackgroundStartInitialDelay), - MaximumDelay: f.fabconnectConf.GetDuration(FabconnectBackgroundStartMaxDelay), - Factor: f.fabconnectConf.GetFloat64(FabconnectBackgroundStartFactor), + f.wsconn[namespace], err = wsclient.New(ctx, f.wsConfig, nil, func(ctx context.Context, w wsclient.WSClient) error { + // Send a subscribe to our topic after each connect/reconnect + b, _ := json.Marshal(&fabWSCommandPayload{ + Type: "listen", + Topic: topic, + }) + err := w.Send(ctx, b) + if err == nil { + b, _ = json.Marshal(&fabWSCommandPayload{ + Type: "listenreplies", + }) + err = w.Send(ctx, b) } - return nil + return err + }) + if err != nil { + return err + } + // Make sure that our event stream is in place + stream, err := f.streams.ensureEventStream(ctx, topic, f.pluginTopic) + if err != nil { + return err } + log.L(f.ctx).Infof("Event stream: %s (topic=%s)", stream.ID, topic) + f.streamID[namespace] = stream.ID - stream, err := f.streams.ensureEventStream(f.ctx, f.topic) + err = f.wsconn[namespace].Connect() if err != nil { return err } - f.streamID = stream.ID - log.L(f.ctx).Infof("Event stream: %s", f.streamID) - f.closed = make(chan struct{}) - go f.eventLoop() + f.closed[namespace] = make(chan struct{}) + + go f.eventLoop(namespace, f.wsconn[namespace], f.closed[namespace]) + + return nil +} + +func (f *Fabric) StopNamespace(ctx context.Context, namespace string) (err error) { + wsconn, ok := f.wsconn[namespace] + if ok { + wsconn.Close() + } + delete(f.wsconn, namespace) + delete(f.streamID, namespace) + delete(f.closed, namespace) return nil } @@ -279,56 +313,10 @@ func (f *Fabric) SetOperationHandler(namespace string, handler core.OperationCal f.callbacks.SetOperationalHandler(namespace, handler) } -func (f *Fabric) backgroundStartLoop() { - _ = f.backgroundRetry.Do(f.ctx, fmt.Sprintf("fabric connector %s", f.Name()), func(attempt int) (retry bool, err error) { - stream, err := f.streams.ensureEventStream(f.ctx, f.topic) - if err != nil { - return true, err - } - - f.streamID = stream.ID - log.L(f.ctx).Infof("Event stream: %s (topic=%s)", f.streamID, f.topic) - - err = f.wsconn.Connect() - if err != nil { - return true, err - } - - f.closed = make(chan struct{}) - go f.eventLoop() - - return false, nil - }) -} - -func (f *Fabric) Start() (err error) { - if f.backgroundStart { - go f.backgroundStartLoop() - return nil - } - return f.wsconn.Connect() -} - func (f *Fabric) Capabilities() *blockchain.Capabilities { return f.capabilities } -func (f *Fabric) afterConnect(ctx context.Context, w wsclient.WSClient) error { - // Send a subscribe to our topic after each connect/reconnect - b, _ := json.Marshal(&fabWSCommandPayload{ - Type: "listen", - Topic: f.topic, - }) - err := w.Send(ctx, b) - if err == nil { - b, _ = json.Marshal(&fabWSCommandPayload{ - Type: "listenreplies", - }) - err = w.Send(ctx, b) - } - return err -} - func decodeJSONPayload(ctx context.Context, payloadString string) *fftypes.JSONObject { bytes, err := base64.StdEncoding.DecodeString(payloadString) if err != nil { @@ -445,7 +433,11 @@ func (f *Fabric) AddFireflySubscription(ctx context.Context, namespace *core.Nam fabricOnChainLocation.Chaincode = "" } - sub, err := f.streams.ensureFireFlySubscription(ctx, namespace.Name, version, fabricOnChainLocation, contract.FirstEvent, f.streamID, batchPinEvent) + streamID, ok := f.streamID[namespace.Name] + if !ok { + return "", i18n.NewError(ctx, coremsgs.MsgInternalServerError, "eventstream ID not found") + } + sub, err := f.streams.ensureFireFlySubscription(ctx, namespace.Name, version, fabricOnChainLocation, contract.FirstEvent, streamID, batchPinEvent) if err != nil { return "", err } @@ -508,17 +500,19 @@ func (f *Fabric) handleMessageBatch(ctx context.Context, messages []interface{}) return f.callbacks.DispatchBlockchainEvents(ctx, events) } -func (f *Fabric) eventLoop() { - defer f.wsconn.Close() - defer close(f.closed) - l := log.L(f.ctx).WithField("role", "event-loop") +func (f *Fabric) eventLoop(namespace string, wsconn wsclient.WSClient, closed chan struct{}) { + topic := f.getTopic(namespace) + defer wsconn.Close() + defer close(closed) + l := log.L(f.ctx).WithField("role", "event-loop").WithField("namespace", namespace) ctx := log.WithLogger(f.ctx, l) + log.L(ctx).Debugf("Starting event loop for namespace '%s'", namespace) for { select { case <-ctx.Done(): l.Debugf("Event loop exiting (context cancelled)") return - case msgBytes, ok := <-f.wsconn.Receive(): + case msgBytes, ok := <-wsconn.Receive(): if !ok { l.Debugf("Event loop exiting (receive channel closed). Terminating server!") f.cancelCtx() @@ -536,12 +530,12 @@ func (f *Fabric) eventLoop() { err = f.handleMessageBatch(ctx, msgTyped) var ackOrNack []byte if err == nil { - ackOrNack, _ = json.Marshal(map[string]string{"type": "ack", "topic": f.topic}) + ackOrNack, _ = json.Marshal(map[string]string{"type": "ack", "topic": topic}) } else { log.L(ctx).Errorf("Rejecting batch due error: %s", err) - ackOrNack, _ = json.Marshal(map[string]string{"type": "error", "topic": f.topic, "message": err.Error()}) + ackOrNack, _ = json.Marshal(map[string]string{"type": "error", "topic": topic, "message": err.Error()}) } - err = f.wsconn.Send(ctx, ackOrNack) + err = wsconn.Send(ctx, ackOrNack) case map[string]interface{}: var receipt common.BlockchainReceiptNotification _ = json.Unmarshal(msgBytes, &receipt) @@ -927,13 +921,14 @@ func encodeContractLocation(ctx context.Context, ntype blockchain.NormalizeType, } func (f *Fabric) AddContractListener(ctx context.Context, listener *core.ContractListener) error { + namespace := listener.Namespace location, err := parseContractLocation(ctx, listener.Location) if err != nil { return err } subName := fmt.Sprintf("ff-sub-%s-%s", listener.Namespace, listener.ID) - result, err := f.streams.createSubscription(ctx, location, f.streamID, subName, listener.Event.Name, listener.Options.FirstEvent) + result, err := f.streams.createSubscription(ctx, location, f.streamID[namespace], subName, listener.Event.Name, listener.Options.FirstEvent) if err != nil { return err } @@ -945,7 +940,7 @@ func (f *Fabric) DeleteContractListener(ctx context.Context, subscription *core. return f.streams.deleteSubscription(ctx, subscription.BackendID, okNotFound) } -func (f *Fabric) GetContractListenerStatus(ctx context.Context, subID string, okNotFound bool) (bool, interface{}, error) { +func (f *Fabric) GetContractListenerStatus(ctx context.Context, namespace, subID string, okNotFound bool) (bool, interface{}, error) { // Fabconnect does not currently provide any additional status info for listener subscriptions. return true, nil, nil } diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index b661a2995..bc4b2cd42 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -33,7 +33,6 @@ import ( "github.com/hyperledger/firefly-common/pkg/fftls" "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-common/pkg/retry" "github.com/hyperledger/firefly-common/pkg/wsclient" "github.com/hyperledger/firefly/internal/blockchain/common" "github.com/hyperledger/firefly/internal/cache" @@ -62,16 +61,17 @@ func resetConf(e *Fabric) { func newTestFabric() (*Fabric, func()) { ctx, cancel := context.WithCancel(context.Background()) - wsm := &wsmocks.WSClient{} f := &Fabric{ ctx: ctx, cancelCtx: cancel, client: resty.New().SetBaseURL("http://localhost:12345"), defaultChannel: "firefly", - topic: "topic1", + pluginTopic: "topic1", prefixShort: defaultPrefixShort, prefixLong: defaultPrefixLong, - wsconn: wsm, + streamID: make(map[string]string), + wsconn: make(map[string]wsclient.WSClient), + closed: make(map[string]chan struct{}), cache: cache.NewUmanagedCache(ctx, 100, 5*time.Minute), callbacks: common.NewBlockchainCallbacks(), subs: common.NewFireflySubscriptions(), @@ -80,7 +80,9 @@ func newTestFabric() (*Fabric, func()) { cancel() if f.closed != nil { // We've init'd, wait to close - <-f.closed + for _, cls := range f.closed { + <-cls + } } } } @@ -141,41 +143,84 @@ func mockNetworkVersion(t *testing.T, version float64) func(req *http.Request) ( } } -func TestInitMissingURL(t *testing.T) { +func TestStartNamespaceWSConnectFail(t *testing.T) { e, cancel := newTestFabric() defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpURL := "http://fftm.example.com:12345" + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/ws", httpURL), + httpmock.NewJsonResponderOrPanic(500, "{}")) + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") cmi := &cachemocks.Manager{} - err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) - assert.Regexp(t, "FF10138.*url", err) + cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) + err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) + assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") + assert.Regexp(t, "FF00148", err) } -func TestInitBackgroundStart(t *testing.T) { - f, cancel := newTestFabric() +func TestStartStopNamespace(t *testing.T) { + e, cancel := newTestFabric() defer cancel() - resetConf(f) - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", func(r *http.Request) (*http.Response, error) { - assert.Fail(t, "Should not call event streams on init") - return &http.Response{}, nil - }) + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{})) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectBackgroundStart, true) utFabconnectConf.Set(FabconnectConfigTopic, "topic1") cmi := &cachemocks.Manager{} - cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(f.ctx, 100, 5*time.Minute), nil) - err := f.Init(f.ctx, f.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) + cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) + err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) + assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + + <-toServer + err = e.StopNamespace(e.ctx, "ns1") assert.NoError(t, err) - assert.Empty(t, f.streamID) +} + +func TestInitMissingURL(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + resetConf(e) + + cmi := &cachemocks.Manager{} + err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) + assert.Regexp(t, "FF10138.*url", err) } func TestGenerateErrorSignatureNoOp(t *testing.T) { @@ -268,24 +313,24 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { assert.Equal(t, core.VerifierTypeMSPIdentity, e.VerifierType()) assert.NoError(t, err) - err = e.Start() + err = e.StartNamespace(e.ctx, "ns1") assert.NoError(t, err) assert.Equal(t, 2, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) + assert.Equal(t, "es12345", e.streamID["ns1"]) assert.NotNil(t, e.Capabilities()) startupMessage := <-toServer - assert.Equal(t, `{"type":"listen","topic":"topic1"}`, startupMessage) + assert.Equal(t, `{"type":"listen","topic":"topic1/ns1"}`, startupMessage) startupMessage = <-toServer assert.Equal(t, `{"type":"listenreplies"}`, startupMessage) fromServer <- `{"bad":"receipt"}` // will be ignored - no ack\ fromServer <- `[]` // empty batch, will be ignored, but acked reply := <-toServer - assert.Equal(t, `{"topic":"topic1","type":"ack"}`, reply) + assert.Equal(t, `{"topic":"topic1/ns1","type":"ack"}`, reply) fromServer <- `[{}]` // bad batch, which will be nack'd reply = <-toServer - assert.Regexp(t, `{\"message\":\"FF10310: .*\",\"topic\":\"topic1\",\"type\":\"error\"}`, reply) + assert.Regexp(t, `{\"message\":\"FF10310: .*\",\"topic\":\"topic1/ns1\",\"type\":\"error\"}`, reply) // Bad data will be ignored fromServer <- `!json` @@ -294,160 +339,81 @@ func TestInitAllNewStreamsAndWSEvent(t *testing.T) { } -func TestBackgroundStart(t *testing.T) { +func TestWSInitFail(t *testing.T) { - log.SetLevel("trace") e, cancel := newTestFabric() defer cancel() - toServer, fromServer, wsURL, done := wsclient.NewTestWSServer(nil) - defer done() - - mockedClient := &http.Client{} - httpmock.ActivateNonDefault(mockedClient) - defer httpmock.DeactivateAndReset() - - u, _ := url.Parse(wsURL) - u.Scheme = "http" - httpURL := u.String() - - httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), - httpmock.NewJsonResponderOrPanic(200, []eventStream{})) - httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL), - httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) - resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) - utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "!!!://") utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - utFabconnectConf.Set(FabconnectBackgroundStart, true) cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) - originalContext := e.ctx err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) - cmi.AssertCalled(t, "GetCache", cache.NewCacheConfig( - originalContext, - coreconfig.CacheBlockchainLimit, - coreconfig.CacheBlockchainTTL, - "", - )) assert.NoError(t, err) - msb := &blockchaincommonmocks.FireflySubscriptions{} - e.subs = msb - msb.On("GetSubscription", mock.Anything).Return(&common.SubscriptionInfo{ - Version: 2, - Extra: "channel1", - }) - - assert.Equal(t, "fabric", e.Name()) - assert.Equal(t, core.VerifierTypeMSPIdentity, e.VerifierType()) - - assert.NoError(t, err) - err = e.Start() - assert.NoError(t, err) - - assert.Eventually(t, func() bool { return httpmock.GetTotalCallCount() == 2 }, time.Second, time.Microsecond) - assert.Eventually(t, func() bool { return e.streamID == "es12345" }, time.Second, time.Microsecond) - assert.NotNil(t, e.Capabilities()) - - startupMessage := <-toServer - assert.Equal(t, `{"type":"listen","topic":"topic1"}`, startupMessage) - startupMessage = <-toServer - assert.Equal(t, `{"type":"listenreplies"}`, startupMessage) - fromServer <- `{"bad":"receipt"}` // will be ignored - no ack\ - fromServer <- `[]` // empty batch, will be ignored, but acked - reply := <-toServer - assert.Equal(t, `{"topic":"topic1","type":"ack"}`, reply) - fromServer <- `[{}]` // bad batch, which will be nack'd - reply = <-toServer - assert.Regexp(t, `{\"message\":\"FF10310: .*\",\"topic\":\"topic1\",\"type\":\"error\"}`, reply) - - // Bad data will be ignored - fromServer <- `!json` - fromServer <- `{"not": "a reply"}` - fromServer <- `42` + err = e.StartNamespace(e.ctx, "ns1") + assert.Regexp(t, "FF00149", err) } -func TestBackgroundStartFail(t *testing.T) { - - log.SetLevel("trace") +func TestCacheInitFail(t *testing.T) { + cacheInitError := errors.New("Initialization error.") e, cancel := newTestFabric() - defer cancel() - - _, _, wsURL, done := wsclient.NewTestWSServer(nil) - defer done() mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - u, _ := url.Parse(wsURL) - u.Scheme = "http" - httpURL := u.String() - - httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), - httpmock.NewJsonResponderOrPanic(500, "Failed to get eventstreams")) + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) + httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.NewJsonResponderOrPanic(200, []subscription{ + {ID: "sub12345", Stream: "es12345", Name: "ns1_BatchPin"}, + })) + httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"), + mockNetworkVersion(t, 2)) resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - utFabconnectConf.Set(FabconnectBackgroundStart, true) - utFabconnectConf.Set(wsclient.WSConfigKeyInitialConnectAttempts, 1) - cmi := &cachemocks.Manager{} - cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) - originalContext := e.ctx - err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) - - cmi.AssertCalled(t, "GetCache", cache.NewCacheConfig( - originalContext, - coreconfig.CacheBlockchainLimit, - coreconfig.CacheBlockchainTTL, - "", - )) - assert.NoError(t, err) - - capturedErr := make(chan error) - e.backgroundRetry = &retry.Retry{ - ErrCallback: func(err error) { - capturedErr <- err - }, - } - - err = e.Start() - assert.NoError(t, err) + cmi.On("GetCache", mock.Anything).Return(nil, cacheInitError) - err = <-capturedErr - assert.Regexp(t, "FF10284", err) + defer cancel() + err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) + assert.Equal(t, cacheInitError, err) } -func TestBackgroundStartWSFail(t *testing.T) { - - log.SetLevel("trace") +func TestInitAllExistingStreams(t *testing.T) { e, cancel := newTestFabric() defer cancel() + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() + mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - u, err := url.Parse("http://localhost:12345") - assert.NoError(t, err) - + u, _ := url.Parse(wsURL) + u.Scheme = "http" httpURL := u.String() httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), - httpmock.NewJsonResponderOrPanic(200, []eventStream{})) - httpmock.RegisterResponder("POST", fmt.Sprintf("%s/eventstreams", httpURL), - httpmock.NewJsonResponderOrPanic(200, eventStream{ID: "es12345"})) + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL), + httpmock.NewJsonResponderOrPanic(200, []subscription{ + {ID: "sub12345", Stream: "es12345", Name: "ns1_BatchPin"}, + })) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/query", httpURL), + mockNetworkVersion(t, 2)) resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) @@ -455,113 +421,60 @@ func TestBackgroundStartWSFail(t *testing.T) { utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - utFabconnectConf.Set(FabconnectBackgroundStart, true) - utFabconnectConf.Set(wsclient.WSConfigKeyInitialConnectAttempts, 1) + + location := fftypes.JSONAnyPtr(fftypes.JSONObject{ + "channel": "firefly", + "chaincode": "simplestorage", + }.String()) + contract := &blockchain.MultipartyContract{ + Location: location, + FirstEvent: "oldest", + } cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) - originalContext := e.ctx - err = e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) - cmi.AssertCalled(t, "GetCache", cache.NewCacheConfig( - originalContext, - coreconfig.CacheBlockchainLimit, - coreconfig.CacheBlockchainTTL, - "", - )) + err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) + ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} - msb := &blockchaincommonmocks.FireflySubscriptions{} - e.subs = msb - msb.On("GetSubscription", mock.Anything).Return(&common.SubscriptionInfo{ - Version: 2, - Extra: "channel1", - }) - - assert.Equal(t, "fabric", e.Name()) - assert.Equal(t, core.VerifierTypeMSPIdentity, e.VerifierType()) + err = e.StartNamespace(e.ctx, ns.Name) + assert.NoError(t, err) - capturedErr := make(chan error) - e.backgroundRetry = &retry.Retry{ - ErrCallback: func(err error) { - capturedErr <- err - }, - } + <-toServer - err = e.Start() + _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) - err = <-capturedErr - assert.Regexp(t, "FF00148", err) + assert.Equal(t, 3, httpmock.GetTotalCallCount()) + assert.Equal(t, "es12345", e.streamID["ns1"]) } -func TestWSInitFail(t *testing.T) { - +func TestInitAllExistingStreamsV1(t *testing.T) { e, cancel := newTestFabric() defer cancel() - resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, "!!!://") - utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") - utFabconnectConf.Set(FabconnectConfigSigner, "signer001") - utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - - cmi := &cachemocks.Manager{} - cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) - err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) - assert.Regexp(t, "FF00149", err) - -} - -func TestCacheInitFail(t *testing.T) { - cacheInitError := errors.New("Initialization error.") - e, cancel := newTestFabric() + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", - httpmock.NewJsonResponderOrPanic(200, []subscription{ - {ID: "sub12345", Stream: "es12345", Name: "ns1_BatchPin"}, - })) - httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"), - mockNetworkVersion(t, 2)) - - resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") - utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) - utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") - utFabconnectConf.Set(FabconnectConfigSigner, "signer001") - utFabconnectConf.Set(FabconnectConfigTopic, "topic1") - cmi := &cachemocks.Manager{} - cmi.On("GetCache", mock.Anything).Return(nil, cacheInitError) - - defer cancel() - err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) - assert.Equal(t, cacheInitError, err) -} - -func TestInitAllExistingStreams(t *testing.T) { - e, cancel := newTestFabric() - defer cancel() - - mockedClient := &http.Client{} - httpmock.ActivateNonDefault(mockedClient) - defer httpmock.DeactivateAndReset() + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, []subscription{ - {ID: "sub12345", Stream: "es12345", Name: "ns1_BatchPin"}, + {ID: "sub12345", Stream: "es12345", Name: "BatchPin"}, })) - httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"), - mockNetworkVersion(t, 2)) + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/query", httpURL), + mockNetworkVersion(t, 1)) resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") @@ -581,31 +494,44 @@ func TestInitAllExistingStreams(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} + + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + + <-toServer + _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) assert.Equal(t, 3, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) + assert.Equal(t, "es12345", e.streamID["ns1"]) } -func TestInitAllExistingStreamsV1(t *testing.T) { +func TestAddFireflySubscriptionGlobal(t *testing.T) { e, cancel := newTestFabric() defer cancel() - - mockedClient := &http.Client{} - httpmock.ActivateNonDefault(mockedClient) - defer httpmock.DeactivateAndReset() + resetConf(e) httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", - httpmock.NewJsonResponderOrPanic(200, []subscription{ - {ID: "sub12345", Stream: "es12345", Name: "BatchPin"}, - })) + httpmock.NewJsonResponderOrPanic(200, []subscription{})) httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"), mockNetworkVersion(t, 1)) - resetConf(e) + httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`, + func(req *http.Request) (*http.Response, error) { + var body map[string]interface{} + json.NewDecoder(req.Body).Decode(&body) + assert.Equal(t, "firefly", body["channel"]) + assert.Equal(t, nil, body["chaincode"]) + return httpmock.NewJsonResponderOrPanic(200, body)(req) + }) + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") @@ -618,7 +544,8 @@ func TestInitAllExistingStreamsV1(t *testing.T) { }.String()) contract := &blockchain.MultipartyContract{ Location: location, - FirstEvent: "oldest", + FirstEvent: "newest", + Options: fftypes.JSONAnyPtr(`{"customPinSupport":true}`), } cmi := &cachemocks.Manager{} @@ -626,14 +553,12 @@ func TestInitAllExistingStreamsV1(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} + e.streamID["ns1"] = "es12345" _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) - - assert.Equal(t, 3, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) } -func TestAddFireflySubscriptionGlobal(t *testing.T) { +func TestAddFireflySubscriptionEventstreamFail(t *testing.T) { e, cancel := newTestFabric() defer cancel() resetConf(e) @@ -680,7 +605,7 @@ func TestAddFireflySubscriptionGlobal(t *testing.T) { assert.NoError(t, err) ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) - assert.NoError(t, err) + assert.Regexp(t, "FF10465", err) } func TestAddFireflySubscriptionBadOptions(t *testing.T) { @@ -720,6 +645,7 @@ func TestAddFireflySubscriptionBadOptions(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} + e.streamID["ns1"] = "es12345" _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.Regexp(t, "pop", err) } @@ -760,6 +686,7 @@ func TestAddFireflySubscriptionQuerySubsFail(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} + e.streamID["ns1"] = "es12345" _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.Regexp(t, "pop", err) } @@ -810,21 +737,28 @@ func TestAddAndRemoveFireflySubscriptionDeprecatedSubName(t *testing.T) { e, cancel := newTestFabric() defer cancel() + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() + mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, []subscription{ {ID: "sub12345", Stream: "es12345", Name: "BatchPin"}, })) - httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"), + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/query", httpURL), mockNetworkVersion(t, 1)) resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") @@ -843,12 +777,18 @@ func TestAddAndRemoveFireflySubscriptionDeprecatedSubName(t *testing.T) { cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + + <-toServer + ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} subID, err := e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) assert.Equal(t, 3, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) + assert.Equal(t, "es12345", e.streamID["ns1"]) assert.NotNil(t, e.subs.GetSubscription(subID)) e.RemoveFireflySubscription(e.ctx, subID) @@ -859,21 +799,28 @@ func TestAddFireflySubscriptionInvalidSubName(t *testing.T) { e, cancel := newTestFabric() defer cancel() + toServer, _, wsURL, done := wsclient.NewTestWSServer(nil) + defer done() + mockedClient := &http.Client{} httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions", + u, _ := url.Parse(wsURL) + u.Scheme = "http" + httpURL := u.String() + + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/eventstreams", httpURL), + httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1/ns1"}})) + httpmock.RegisterResponder("GET", fmt.Sprintf("%s/subscriptions", httpURL), httpmock.NewJsonResponderOrPanic(200, []subscription{ {ID: "sub12345", Stream: "es12345", Name: "BatchPin"}, })) - httpmock.RegisterResponder("POST", fmt.Sprintf("http://localhost:12345/query"), + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/query", httpURL), mockNetworkVersion(t, 2)) resetConf(e) - utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPConfigURL, httpURL) utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") utFabconnectConf.Set(FabconnectConfigSigner, "signer001") @@ -892,6 +839,12 @@ func TestAddFireflySubscriptionInvalidSubName(t *testing.T) { cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") + assert.NoError(t, err) + + <-toServer + ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.Regexp(t, "FF10416", err) @@ -919,9 +872,6 @@ func TestInitNewConfig(t *testing.T) { httpmock.ActivateNonDefault(mockedClient) defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", - httpmock.NewJsonResponderOrPanic(200, []eventStream{{ID: "es12345", Name: "topic1"}})) - resetConf(e) utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) @@ -932,9 +882,6 @@ func TestInitNewConfig(t *testing.T) { cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) - - assert.Equal(t, 1, httpmock.GetTotalCallCount()) - assert.Equal(t, "es12345", e.streamID) } func TestStreamQueryError(t *testing.T) { @@ -960,6 +907,9 @@ func TestStreamQueryError(t *testing.T) { cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) + assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") assert.Regexp(t, "FF10284.*pop", err) } @@ -989,10 +939,115 @@ func TestStreamCreateError(t *testing.T) { cmi := &cachemocks.Manager{} cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) + assert.NoError(t, err) + + err = e.StartNamespace(e.ctx, "ns1") assert.Regexp(t, "FF10284.*pop", err) } +func TestEnsureStreamDelete(t *testing.T) { + + e, cancel := newTestFabric() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{ + { + ID: "es12345", + Name: "topic1", + }, + })) + httpmock.RegisterResponder("DELETE", "http://localhost:12345/eventstreams/es12345", + httpmock.NewJsonResponderOrPanic(204, "")) + httpmock.RegisterResponder("POST", "http://localhost:12345/eventstreams", + httpmock.NewStringResponder(200, "")) + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") + utFabconnectConf.Set(FabconnectConfigSigner, "signer001") + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) + err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) + assert.NoError(t, err) + + _, err = e.streams.ensureEventStream(context.Background(), "topic1/ns1", "topic1") + assert.NoError(t, err) +} + +func TestEnsureStreamDeleteFail(t *testing.T) { + + e, cancel := newTestFabric() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://localhost:12345/eventstreams", + httpmock.NewJsonResponderOrPanic(200, []eventStream{ + { + ID: "es12345", + Name: "topic1", + }, + })) + httpmock.RegisterResponder("DELETE", "http://localhost:12345/eventstreams/es12345", + httpmock.NewJsonResponderOrPanic(500, "pop")) + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") + utFabconnectConf.Set(FabconnectConfigSigner, "signer001") + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) + err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) + assert.NoError(t, err) + + _, err = e.streams.ensureEventStream(context.Background(), "topic1/ns1", "topic1") + assert.Regexp(t, "FF10284.*pop", err) +} + +func TestDeleteStreamOKNotFound(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + + mockedClient := &http.Client{} + httpmock.ActivateNonDefault(mockedClient) + defer httpmock.DeactivateAndReset() + + httpURL := "http://localhost:12345" + + httpmock.RegisterResponder("DELETE", fmt.Sprintf("%s/eventstreams/es12345", httpURL), + httpmock.NewJsonResponderOrPanic(404, "pop")) + + resetConf(e) + utFabconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345") + utFabconnectConf.Set(ffresty.HTTPConfigRetryEnabled, false) + utFabconnectConf.Set(ffresty.HTTPCustomClient, mockedClient) + utFabconnectConf.Set(FabconnectConfigChaincodeDeprecated, "firefly") + utFabconnectConf.Set(FabconnectConfigSigner, "signer001") + utFabconnectConf.Set(FabconnectConfigTopic, "topic1") + + cmi := &cachemocks.Manager{} + cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(e.ctx, 100, 5*time.Minute), nil) + err := e.Init(e.ctx, e.cancelCtx, utConfig, e.metrics, cmi) + assert.NoError(t, err) + + err = e.streams.deleteEventStream(context.Background(), "es12345", true) + assert.NoError(t, err) +} + func TestSubQueryCreateError(t *testing.T) { e, cancel := newTestFabric() @@ -1035,6 +1090,7 @@ func TestSubQueryCreateError(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} + e.streamID["ns1"] = "es12345" _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.Regexp(t, "FF10284.*pop", err) @@ -1082,6 +1138,7 @@ func TestSubQueryCreate(t *testing.T) { err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi) assert.NoError(t, err) ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"} + e.streamID["ns1"] = "es12345" _, err = e.AddFireflySubscription(e.ctx, ns, contract) assert.NoError(t, err) @@ -1627,23 +1684,25 @@ func TestEventLoopContextCancelled(t *testing.T) { e, cancel := newTestFabric() cancel() r := make(<-chan []byte) - wsm := e.wsconn.(*wsmocks.WSClient) + wsm := &wsmocks.WSClient{} + e.wsconn["ns1"] = wsm wsm.On("Receive").Return(r) wsm.On("Close").Return() - e.closed = make(chan struct{}) - e.eventLoop() // we're simply looking for it exiting + e.closed["ns1"] = make(chan struct{}) + e.eventLoop("ns1", wsm, e.closed["ns1"]) // we're simply looking for it exiting } func TestEventLoopReceiveClosed(t *testing.T) { e, cancel := newTestFabric() defer cancel() r := make(chan []byte) - wsm := e.wsconn.(*wsmocks.WSClient) + wsm := &wsmocks.WSClient{} + e.wsconn["ns1"] = wsm close(r) wsm.On("Receive").Return((<-chan []byte)(r)) wsm.On("Close").Return() - e.closed = make(chan struct{}) - e.eventLoop() // we're simply looking for it exiting + e.closed["ns1"] = make(chan struct{}) + e.eventLoop("ns1", wsm, e.closed["ns1"]) // we're simply looking for it exiting } func TestEventLoopSendClosed(t *testing.T) { @@ -1651,15 +1710,16 @@ func TestEventLoopSendClosed(t *testing.T) { s := make(chan []byte, 1) s <- []byte(`[]`) r := make(chan []byte) - wsm := e.wsconn.(*wsmocks.WSClient) + wsm := &wsmocks.WSClient{} + e.wsconn["ns1"] = wsm wsm.On("Receive").Return((<-chan []byte)(s)) wsm.On("Close").Return() wsm.On("Send", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { go cancel() close(r) }) - e.closed = make(chan struct{}) - e.eventLoop() // we're simply looking for it exiting + e.closed["ns1"] = make(chan struct{}) + e.eventLoop("ns1", wsm, e.closed["ns1"]) // we're simply looking for it exiting wsm.AssertExpectations(t) } @@ -1667,10 +1727,11 @@ func TestEventLoopUnexpectedMessage(t *testing.T) { e, cancel := newTestFabric() defer cancel() r := make(chan []byte) - wsm := e.wsconn.(*wsmocks.WSClient) + wsm := &wsmocks.WSClient{} + e.wsconn["ns1"] = wsm wsm.On("Receive").Return((<-chan []byte)(r)) wsm.On("Close").Return() - e.closed = make(chan struct{}) + e.closed["ns1"] = make(chan struct{}) operationID := fftypes.NewUUID() data := []byte(`{ "_id": "6fb94fff-81d3-4094-567d-e031b1871694", @@ -1700,7 +1761,7 @@ func TestEventLoopUnexpectedMessage(t *testing.T) { close(done) } - go e.eventLoop() + go e.eventLoop("ns1", wsm, e.closed["ns1"]) r <- []byte(`!badjson`) // ignored bad json r <- []byte(`"not an object"`) // ignored wrong type r <- data @@ -1711,11 +1772,11 @@ func TestHandleReceiptTXSuccess(t *testing.T) { em := &coremocks.OperationCallbacks{} wsm := &wsmocks.WSClient{} e := &Fabric{ - ctx: context.Background(), - topic: "topic1", - callbacks: common.NewBlockchainCallbacks(), - subs: common.NewFireflySubscriptions(), - wsconn: wsm, + ctx: context.Background(), + pluginTopic: "topic1", + callbacks: common.NewBlockchainCallbacks(), + subs: common.NewFireflySubscriptions(), + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, } e.SetOperationHandler("ns1", em) @@ -1753,11 +1814,11 @@ func TestHandleReceiptNoRequestID(t *testing.T) { em := &blockchainmocks.Callbacks{} wsm := &wsmocks.WSClient{} e := &Fabric{ - ctx: context.Background(), - topic: "topic1", - callbacks: common.NewBlockchainCallbacks(), - subs: common.NewFireflySubscriptions(), - wsconn: wsm, + ctx: context.Background(), + pluginTopic: "topic1", + callbacks: common.NewBlockchainCallbacks(), + subs: common.NewFireflySubscriptions(), + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, } e.SetHandler("ns1", em) @@ -1772,11 +1833,11 @@ func TestHandleReceiptFailedTx(t *testing.T) { em := &coremocks.OperationCallbacks{} wsm := &wsmocks.WSClient{} e := &Fabric{ - ctx: context.Background(), - topic: "topic1", - callbacks: common.NewBlockchainCallbacks(), - subs: common.NewFireflySubscriptions(), - wsconn: wsm, + ctx: context.Background(), + pluginTopic: "topic1", + callbacks: common.NewBlockchainCallbacks(), + subs: common.NewFireflySubscriptions(), + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, } e.SetOperationHandler("ns1", em) @@ -1820,7 +1881,7 @@ func TestAddSubscription(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -1855,7 +1916,7 @@ func TestAddSubscriptionNoChannel(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -1889,7 +1950,7 @@ func TestAddSubscriptionNoLocation(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -1912,7 +1973,7 @@ func TestAddSubscriptionBadLocation(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -1933,7 +1994,7 @@ func TestAddSubscriptionFail(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -1963,7 +2024,7 @@ func TestDeleteSubscription(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -1986,7 +2047,7 @@ func TestDeleteSubscriptionFail(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -2009,7 +2070,7 @@ func TestDeleteSubscriptionNotFound(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - e.streamID = "es-1" + e.streamID["ns1"] = "es-1" e.streams = &streamManager{ client: e.client, } @@ -3189,7 +3250,7 @@ func TestGetContractListenerStatus(t *testing.T) { httpmock.ActivateNonDefault(e.client.GetClient()) defer httpmock.DeactivateAndReset() - _, status, err := e.GetContractListenerStatus(context.Background(), "id", true) + _, status, err := e.GetContractListenerStatus(context.Background(), "ns1", "id", true) assert.Nil(t, status) assert.NoError(t, err) } diff --git a/internal/blockchain/tezos/tezos.go b/internal/blockchain/tezos/tezos.go index f5dd984d4..cb3c20482 100644 --- a/internal/blockchain/tezos/tezos.go +++ b/internal/blockchain/tezos/tezos.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -230,6 +230,16 @@ func (t *Tezos) Init(ctx context.Context, cancelCtx context.CancelFunc, conf con return nil } +func (t *Tezos) StartNamespace(ctx context.Context, namespace string) (err error) { + // TODO: Implement + return nil +} + +func (t *Tezos) StopNamespace(ctx context.Context, namespace string) (err error) { + // TODO: Implement + return nil +} + func (t *Tezos) SetHandler(namespace string, handler blockchain.Callbacks) { t.callbacks.SetHandler(namespace, handler) } @@ -391,7 +401,7 @@ func (t *Tezos) DeleteContractListener(ctx context.Context, subscription *core.C } // Note: In state of development. Approach can be changed. -func (t *Tezos) GetContractListenerStatus(ctx context.Context, subID string, okNotFound bool) (found bool, status interface{}, err error) { +func (t *Tezos) GetContractListenerStatus(ctx context.Context, namespace, subID string, okNotFound bool) (found bool, status interface{}, err error) { sub, err := t.streams.getSubscription(ctx, subID, okNotFound) if err != nil || sub == nil { return false, nil, err diff --git a/internal/blockchain/tezos/tezos_test.go b/internal/blockchain/tezos/tezos_test.go index 4f0ec8f96..47671e616 100644 --- a/internal/blockchain/tezos/tezos_test.go +++ b/internal/blockchain/tezos/tezos_test.go @@ -1650,7 +1650,7 @@ func TestGetContractListenerStatus(t *testing.T) { err := tz.Init(tz.ctx, tz.cancelCtx, utConfig, tz.metrics, cmi) assert.NoError(t, err) - found, status, err := tz.GetContractListenerStatus(context.Background(), "sub1", true) + found, status, err := tz.GetContractListenerStatus(context.Background(), "ns1", "sub1", true) assert.NotNil(t, status) assert.NoError(t, err) assert.True(t, found) @@ -1683,7 +1683,7 @@ func TestGetContractListenerStatusGetSubFail(t *testing.T) { err := tz.Init(tz.ctx, tz.cancelCtx, utConfig, tz.metrics, cmi) assert.NoError(t, err) - found, status, err := tz.GetContractListenerStatus(context.Background(), "sub1", true) + found, status, err := tz.GetContractListenerStatus(context.Background(), "ns1", "sub1", true) assert.Nil(t, status) assert.Regexp(t, "FF10283", err) assert.False(t, found) @@ -1716,7 +1716,7 @@ func TestGetContractListenerStatusGetSubNotFound(t *testing.T) { err := tz.Init(tz.ctx, tz.cancelCtx, utConfig, tz.metrics, cmi) assert.NoError(t, err) - found, status, err := tz.GetContractListenerStatus(context.Background(), "sub1", true) + found, status, err := tz.GetContractListenerStatus(context.Background(), "ns1", "sub1", true) assert.Nil(t, status) assert.Nil(t, err) assert.False(t, found) @@ -1882,3 +1882,17 @@ func TestSubmitBatchPin(t *testing.T) { err := tz.SubmitBatchPin(context.Background(), "", "", singer, nil, location) assert.NoError(t, err) } + +func TestStartNamespace(t *testing.T) { + tz, cancel := newTestTezos() + defer cancel() + err := tz.StartNamespace(context.Background(), "ns1") + assert.NoError(t, err) +} + +func TestStopNamespace(t *testing.T) { + tz, cancel := newTestTezos() + defer cancel() + err := tz.StopNamespace(context.Background(), "ns1") + assert.NoError(t, err) +} diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index ace0a4437..359e66795 100644 --- a/internal/contracts/manager.go +++ b/internal/contracts/manager.go @@ -794,7 +794,7 @@ func (cm *contractManager) resolveEvent(ctx context.Context, ffi *fftypes.FFIRef } func (cm *contractManager) checkContractListenerExists(ctx context.Context, listener *core.ContractListener) error { - found, _, err := cm.blockchain.GetContractListenerStatus(ctx, listener.BackendID, true) + found, _, err := cm.blockchain.GetContractListenerStatus(ctx, listener.Namespace, listener.BackendID, true) if err != nil { log.L(ctx).Errorf("Validating listener %s:%s (BackendID=%s) failed: %s", listener.Signature, listener.ID, listener.BackendID, err) return err @@ -939,7 +939,7 @@ func (cm *contractManager) GetContractListenerByNameOrIDWithStatus(ctx context.C if err != nil { return nil, err } - _, status, err := cm.blockchain.GetContractListenerStatus(ctx, listener.BackendID, false) + _, status, err := cm.blockchain.GetContractListenerStatus(ctx, listener.Namespace, listener.BackendID, false) if err != nil { status = core.ListenerStatusError{ StatusError: err.Error(), diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index 304d05933..9587a65d5 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -1064,8 +1064,8 @@ func TestAddContractListenerVerifyOk(t *testing.T) { fi, _ := f.Finalize() return fi.Skip == 0 && fi.Limit == 50 })).Return([]*core.ContractListener{ - {ID: fftypes.NewUUID(), BackendID: "12345"}, - {ID: fftypes.NewUUID(), BackendID: "23456"}, + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"}, + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "23456"}, }, nil, nil).Once() mdi.On("GetContractListeners", mock.Anything, "ns1", mock.MatchedBy(func(f ffapi.Filter) bool { fi, _ := f.Finalize() @@ -1073,8 +1073,8 @@ func TestAddContractListenerVerifyOk(t *testing.T) { })).Return([]*core.ContractListener{}, nil, nil).Once() mbi := cm.blockchain.(*blockchainmocks.Plugin) - mbi.On("GetContractListenerStatus", ctx, "12345", true).Return(true, struct{}{}, nil) - mbi.On("GetContractListenerStatus", ctx, "23456", true).Return(false, nil, nil) + mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, nil) + mbi.On("GetContractListenerStatus", ctx, "ns1", "23456", true).Return(false, nil, nil) mbi.On("AddContractListener", ctx, mock.MatchedBy(func(l *core.ContractListener) bool { prevBackendID := l.BackendID l.BackendID = "34567" @@ -1103,13 +1103,13 @@ func TestAddContractListenerVerifyUpdateFail(t *testing.T) { fi, _ := f.Finalize() return fi.Skip == 0 && fi.Limit == 50 })).Return([]*core.ContractListener{ - {ID: fftypes.NewUUID(), BackendID: "12345"}, - {ID: fftypes.NewUUID(), BackendID: "23456"}, + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"}, + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "23456"}, }, nil, nil).Once() mbi := cm.blockchain.(*blockchainmocks.Plugin) - mbi.On("GetContractListenerStatus", ctx, "12345", true).Return(true, struct{}{}, nil) - mbi.On("GetContractListenerStatus", ctx, "23456", true).Return(false, nil, nil) + mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, nil) + mbi.On("GetContractListenerStatus", ctx, "ns1", "23456", true).Return(false, nil, nil) mbi.On("AddContractListener", ctx, mock.MatchedBy(func(l *core.ContractListener) bool { prevBackendID := l.BackendID l.BackendID = "34567" @@ -1138,13 +1138,13 @@ func TestAddContractListenerVerifyAddFail(t *testing.T) { fi, _ := f.Finalize() return fi.Skip == 0 && fi.Limit == 50 })).Return([]*core.ContractListener{ - {ID: fftypes.NewUUID(), BackendID: "12345"}, - {ID: fftypes.NewUUID(), BackendID: "23456"}, + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"}, + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "23456"}, }, nil, nil).Once() mbi := cm.blockchain.(*blockchainmocks.Plugin) - mbi.On("GetContractListenerStatus", ctx, "12345", true).Return(true, struct{}{}, nil) - mbi.On("GetContractListenerStatus", ctx, "23456", true).Return(false, nil, nil) + mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, nil) + mbi.On("GetContractListenerStatus", ctx, "ns1", "23456", true).Return(false, nil, nil) mbi.On("AddContractListener", ctx, mock.MatchedBy(func(l *core.ContractListener) bool { prevBackendID := l.BackendID l.BackendID = "34567" @@ -1168,12 +1168,12 @@ func TestAddContractListenerVerifyGetFail(t *testing.T) { fi, _ := f.Finalize() return fi.Skip == 0 && fi.Limit == 50 })).Return([]*core.ContractListener{ - {ID: fftypes.NewUUID(), BackendID: "12345"}, - {ID: fftypes.NewUUID(), BackendID: "23456"}, + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"}, + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "23456"}, }, nil, nil).Once() mbi := cm.blockchain.(*blockchainmocks.Plugin) - mbi.On("GetContractListenerStatus", ctx, "12345", true).Return(false, nil, fmt.Errorf("pop")) + mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(false, nil, fmt.Errorf("pop")) err := cm.verifyListeners(ctx) assert.Regexp(t, "pop", err) @@ -2878,8 +2878,8 @@ func TestGetContractListenerByNameOrIDWithStatus(t *testing.T) { id := fftypes.NewUUID() backendID := "testID" - mdi.On("GetContractListenerByID", context.Background(), "ns1", id).Return(&core.ContractListener{BackendID: backendID}, nil) - mbi.On("GetContractListenerStatus", context.Background(), backendID, false).Return(true, fftypes.JSONAnyPtr(fftypes.JSONObject{}.String()), nil) + mdi.On("GetContractListenerByID", context.Background(), "ns1", id).Return(&core.ContractListener{Namespace: "ns1", BackendID: backendID}, nil) + mbi.On("GetContractListenerStatus", context.Background(), "ns1", backendID, false).Return(true, fftypes.JSONAnyPtr(fftypes.JSONObject{}.String()), nil) _, err := cm.GetContractListenerByNameOrIDWithStatus(context.Background(), id.String()) assert.NoError(t, err) @@ -2903,8 +2903,8 @@ func TestGetContractListenerByNameOrIDWithStatusPluginFail(t *testing.T) { id := fftypes.NewUUID() backendID := "testID" - mdi.On("GetContractListenerByID", context.Background(), "ns1", id).Return(&core.ContractListener{BackendID: backendID}, nil) - mbi.On("GetContractListenerStatus", context.Background(), backendID, false).Return(false, nil, fmt.Errorf("pop")) + mdi.On("GetContractListenerByID", context.Background(), "ns1", id).Return(&core.ContractListener{Namespace: "ns1", BackendID: backendID}, nil) + mbi.On("GetContractListenerStatus", context.Background(), "ns1", backendID, false).Return(false, nil, fmt.Errorf("pop")) listener, err := cm.GetContractListenerByNameOrIDWithStatus(context.Background(), id.String()) diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index 7ad5cec26..a01606f95 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -303,4 +303,5 @@ var ( MsgWSWrongNamespace = ffe("FF10462", "Websocket request received on a namespace scoped connection but the provided namespace does not match") MsgMaxSubscriptionEventScanLimitBreached = ffe("FF10463", "Event scan limit breached with start sequence ID %d and end sequence ID %d. Please restrict your query to a narrower range", 400) MsgSequenceIDDidNotParseToInt = ffe("FF10464", "Could not parse provided %s to an integer sequence ID", 400) + MsgInternalServerError = ffe("FF10465", "Internal server error: %s", 500) ) diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go index 7fa7ef1a5..dec69f446 100644 --- a/internal/events/token_pool_created.go +++ b/internal/events/token_pool_created.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -87,7 +87,27 @@ func (em *eventManager) getPoolByIDOrLocator(ctx context.Context, id *fftypes.UU } func (em *eventManager) loadExisting(ctx context.Context, pool *tokens.TokenPool) (existingPool *core.TokenPool, err error) { - if existingPool, err = em.getPoolByIDOrLocator(ctx, pool.ID, pool.Connector, pool.PoolLocator); err != nil || existingPool == nil { + if existingPool, err = em.getPoolByIDOrLocator(ctx, pool.ID, pool.Connector, pool.PoolLocator); err != nil { + return nil, err + } + + if existingPool == nil { + for _, alternateLocator := range pool.AlternateLocators { + if existingPool, err = em.getPoolByIDOrLocator(ctx, pool.ID, pool.Connector, alternateLocator); err != nil { + return existingPool, err + } + if existingPool != nil { + log.L(ctx).Debugf("Updating locator for existing pool ns=%s connector=%s oldLocator=%s newLocator=%s", em.namespace.Name, pool.Connector, existingPool.Locator, pool.PoolLocator) + existingPool.Locator = pool.PoolLocator + if err := em.database.UpsertTokenPool(ctx, existingPool, database.UpsertOptimizationExisting); err != nil { + return existingPool, err + } + break + } + } + } + + if existingPool == nil { log.L(ctx).Debugf("Pool not found with ns=%s connector=%s locator=%s (err=%v)", em.namespace.Name, pool.Connector, pool.PoolLocator, err) return existingPool, err } diff --git a/internal/events/token_pool_created_test.go b/internal/events/token_pool_created_test.go index 2f60f9a3e..638182b23 100644 --- a/internal/events/token_pool_created_test.go +++ b/internal/events/token_pool_created_test.go @@ -531,3 +531,77 @@ func TestTokenPoolCreatedPublishBadSymbol(t *testing.T) { mti.AssertExpectations(t) } + +func TestLoadExistingAlternateLocator(t *testing.T) { + em := newTestEventManager(t) + defer em.cleanup(t) + + existingPool := &core.TokenPool{ + Type: core.TokenTypeFungible, + Locator: "123", + Connector: "erc1155", + Symbol: "ETH", + } + updatedPool := &tokens.TokenPool{ + Type: core.TokenTypeFungible, + PoolLocator: "456", + AlternateLocators: []string{"123"}, + Connector: "erc1155", + Symbol: "ETH", + } + + em.mam.On("GetTokenPoolByLocator", em.ctx, "erc1155", "456").Return(nil, nil).Once() + em.mam.On("GetTokenPoolByLocator", em.ctx, "erc1155", "123").Return(existingPool, nil).Once() + em.mdi.On("UpsertTokenPool", em.ctx, existingPool, database.UpsertOptimizationExisting).Return(nil).Once() + + p, err := em.loadExisting(em.ctx, updatedPool) + assert.NoError(t, err) + assert.Equal(t, p, existingPool) +} + +func TestLoadExistingAlternateLocatorError(t *testing.T) { + em := newTestEventManager(t) + defer em.cleanup(t) + + updatedPool := &tokens.TokenPool{ + Type: core.TokenTypeFungible, + PoolLocator: "456", + AlternateLocators: []string{"123"}, + Connector: "erc1155", + Symbol: "ETH", + } + + em.mam.On("GetTokenPoolByLocator", em.ctx, "erc1155", "456").Return(nil, nil).Once() + em.mam.On("GetTokenPoolByLocator", em.ctx, "erc1155", "123").Return(nil, fmt.Errorf("pop")).Once() + + p, err := em.loadExisting(em.ctx, updatedPool) + assert.Equal(t, err.Error(), "pop") + assert.Nil(t, p) +} + +func TestLoadExistingAlternateLocatorUpsertError(t *testing.T) { + em := newTestEventManager(t) + defer em.cleanup(t) + + existingPool := &core.TokenPool{ + Type: core.TokenTypeFungible, + Locator: "123", + Connector: "erc1155", + Symbol: "ETH", + } + updatedPool := &tokens.TokenPool{ + Type: core.TokenTypeFungible, + PoolLocator: "456", + AlternateLocators: []string{"123"}, + Connector: "erc1155", + Symbol: "ETH", + } + + em.mam.On("GetTokenPoolByLocator", em.ctx, "erc1155", "456").Return(nil, nil).Once() + em.mam.On("GetTokenPoolByLocator", em.ctx, "erc1155", "123").Return(existingPool, nil).Once() + em.mdi.On("UpsertTokenPool", em.ctx, existingPool, database.UpsertOptimizationExisting).Return(fmt.Errorf("pop")).Once() + + p, err := em.loadExisting(em.ctx, updatedPool) + assert.Equal(t, err.Error(), "pop") + assert.Equal(t, p, existingPool) +} diff --git a/internal/namespace/configreload_test.go b/internal/namespace/configreload_test.go index f10bd1869..9afd00b91 100644 --- a/internal/namespace/configreload_test.go +++ b/internal/namespace/configreload_test.go @@ -424,9 +424,7 @@ func mockInitConfig(nmm *nmMocks) { nmm.mdi.On("GetNamespace", mock.Anything, "ns3").Return(nil, nil).Maybe() nmm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil) nmm.mai.On("Init", mock.Anything, mock.Anything, mock.Anything).Return(nil) - nmm.mbi.On("Start").Return(nil) nmm.mdx.On("Start").Return(nil) - nmm.mti[1].On("Start").Return(nil) nmm.mo.On("PreInit", mock.Anything, mock.Anything, mock.Anything).Return() nmm.mo.On("Init"). @@ -817,6 +815,7 @@ func mockPurge(nmm *nmMocks, nsName string) { for _, mti := range nmm.mti { mti.On("SetHandler", nsName, matchNil).Return().Maybe() mti.On("SetOperationHandler", nsName, matchNil).Return().Maybe() + mti.On("StartNamespace", mock.Anything, nsName).Return(nil).Maybe() } } diff --git a/internal/namespace/manager.go b/internal/namespace/manager.go index 018b04e5d..65a82714a 100644 --- a/internal/namespace/manager.go +++ b/internal/namespace/manager.go @@ -409,19 +409,10 @@ func (nm *namespaceManager) startNamespacesAndPlugins(namespacesToStart map[stri go nm.namespaceStarter(ns) } for _, plugin := range pluginsToStart { - switch plugin.category { - case pluginCategoryBlockchain: - if err := plugin.blockchain.Start(); err != nil { - return err - } - case pluginCategoryDataexchange: + if plugin.category == pluginCategoryDataexchange { if err := plugin.dataexchange.Start(); err != nil { return err } - case pluginCategoryTokens: - if err := plugin.tokens.Start(); err != nil { - return err - } } } return nil diff --git a/internal/namespace/manager_test.go b/internal/namespace/manager_test.go index 6dd09e002..9dce67d2b 100644 --- a/internal/namespace/manager_test.go +++ b/internal/namespace/manager_test.go @@ -1851,10 +1851,7 @@ func TestStart(t *testing.T) { waitInit := namespaceInitWaiter(t, nmm, []string{"default"}) - nmm.mbi.On("Start", mock.Anything).Return(nil) nmm.mdx.On("Start", mock.Anything).Return(nil) - nmm.mti[0].On("Start", mock.Anything).Return(nil) - nmm.mti[1].On("Start", mock.Anything).Return(nil) nmm.mdi.On("GetNamespace", mock.Anything, "default").Return(nil, nil) nmm.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil) nmm.mo.On("PreInit", mock.Anything, mock.Anything).Return(nil) @@ -1869,20 +1866,6 @@ func TestStart(t *testing.T) { waitInit.Wait() } -func TestStartBlockchainFail(t *testing.T) { - nm, nmm, cleanup := newTestNamespaceManager(t, true) - defer cleanup() - - nm.namespaces = nil - nmm.mbi.On("Start").Return(fmt.Errorf("pop")) - - err := nm.startNamespacesAndPlugins(nm.namespaces, map[string]*plugin{ - "ethereum": nm.plugins["ethereum"], - }) - assert.EqualError(t, err, "pop") - -} - func TestStartDataExchangeFail(t *testing.T) { nm, nmm, cleanup := newTestNamespaceManager(t, true) defer cleanup() @@ -1897,20 +1880,6 @@ func TestStartDataExchangeFail(t *testing.T) { } -func TestStartTokensFail(t *testing.T) { - nm, nmm, cleanup := newTestNamespaceManager(t, true) - defer cleanup() - - nm.namespaces = nil - nmm.mti[0].On("Start").Return(fmt.Errorf("pop")) - - err := nm.startNamespacesAndPlugins(nm.namespaces, map[string]*plugin{ - "erc721": nm.plugins["erc721"], - }) - assert.EqualError(t, err, "pop") - -} - func TestStartOrchestratorFail(t *testing.T) { nm, nmm, cleanup := newTestNamespaceManager(t, true) defer cleanup() diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 3b599b915..7b2738997 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -306,6 +306,16 @@ func (or *orchestrator) WaitStop() { if !or.started { return } + err := or.plugins.Blockchain.Plugin.StopNamespace(or.ctx, or.namespace.Name) + if err != nil { + log.L(or.ctx).Errorf("Error purging namespace '%s' from blockchain plugin '%s': %s", or.namespace.Name, or.plugins.Blockchain.Name, err.Error()) + } + for _, t := range or.plugins.Tokens { + err := t.Plugin.StopNamespace(or.ctx, or.namespace.Name) + if err != nil { + log.L(or.ctx).Errorf("Error purging namespace '%s' from tokens plugin '%s': %s", or.namespace.Name, t.Name, err.Error()) + } + } if or.batch != nil { or.batch.WaitStop() or.batch = nil @@ -534,6 +544,9 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) { if err != nil { return err } + if err := or.assets.Start(ctx); err != nil { + return err + } } if or.defsender == nil { @@ -554,6 +567,13 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) { } func (or *orchestrator) initComponents(ctx context.Context) (err error) { + if or.blockchain() != nil { + err = or.blockchain().StartNamespace(ctx, or.namespace.Name) + if err != nil { + return err + } + } + if or.data == nil { or.data, err = data.NewDataManager(ctx, or.namespace, or.database(), or.dataexchange(), or.cacheManager) if err != nil { diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 695a6e374..e7ddff253 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -231,6 +231,7 @@ func TestInitOK(t *testing.T) { or.mdi.On("SetHandler", "ns", mock.Anything).Return() or.mbi.On("SetHandler", "ns", mock.Anything).Return() or.mbi.On("SetOperationHandler", "ns", mock.Anything).Return() + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mdi.On("GetIdentities", mock.Anything, "ns", mock.Anything).Return([]*core.Identity{node}, nil, nil) or.mdx.On("SetHandler", "ns2", "node1", mock.Anything).Return() or.mdx.On("SetOperationHandler", "ns", mock.Anything).Return() @@ -298,26 +299,51 @@ func TestInitMessagingComponentFail(t *testing.T) { defer or.cleanup(t) or.plugins.Database.Plugin = nil or.messaging = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) } +func TestInitTokensFail(t *testing.T) { + or := newTestOrchestrator() + defer or.cleanup(t) + or.assets = nil + or.mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything) + or.mmp.On("ConfigureContract", mock.Anything).Return(nil) + or.mdi.On("GetTokenPools", mock.Anything, "ns", mock.Anything).Return([]*core.TokenPool{}, nil, nil) + or.mti.On("StartNamespace", mock.Anything, "ns", mock.Anything).Return(fmt.Errorf("pop")) + err := or.initManagers(context.Background()) + assert.Regexp(t, "pop", err) +} + func TestInitEventsComponentFail(t *testing.T) { or := newTestOrchestrator() defer or.cleanup(t) or.plugins.Database.Plugin = nil or.events = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) } +func TestInitEventsComponentStartNamespaceFail(t *testing.T) { + or := newTestOrchestrator() + defer or.cleanup(t) + or.plugins.Database.Plugin = nil + or.events = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(fmt.Errorf("pop")) + err := or.initComponents(context.Background()) + assert.Regexp(t, "pop", err) +} + func TestInitNetworkMapComponentFail(t *testing.T) { or := newTestOrchestrator() defer or.cleanup(t) or.plugins.Database.Plugin = nil or.networkmap = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) @@ -326,6 +352,7 @@ func TestInitNetworkMapComponentFail(t *testing.T) { func TestInitMultipartyComponentFail(t *testing.T) { or := newTestOrchestrator() defer or.cleanup(t) + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.plugins.Database.Plugin = nil or.multiparty = nil err := or.initComponents(context.Background()) @@ -335,6 +362,7 @@ func TestInitMultipartyComponentFail(t *testing.T) { func TestInitMultipartyComponentConfigureFail(t *testing.T) { or := newTestOrchestrator() defer or.cleanup(t) + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) err := or.initComponents(context.Background()) assert.EqualError(t, err, "pop") @@ -345,6 +373,7 @@ func TestInitSharedStorageDownloadComponentFail(t *testing.T) { defer or.cleanup(t) or.plugins.Database.Plugin = nil or.sharedDownload = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) @@ -355,6 +384,7 @@ func TestInitBatchComponentFail(t *testing.T) { defer or.cleanup(t) or.plugins.Database.Plugin = nil or.batch = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) @@ -365,6 +395,7 @@ func TestInitBroadcastComponentFail(t *testing.T) { defer or.cleanup(t) or.plugins.Database.Plugin = nil or.broadcast = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) @@ -385,6 +416,7 @@ func TestInitDataComponentFail(t *testing.T) { defer or.cleanup(t) or.plugins.Database.Plugin = nil or.data = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) } @@ -394,6 +426,7 @@ func TestInitIdentityComponentFail(t *testing.T) { defer or.cleanup(t) or.plugins.Database.Plugin = nil or.identity = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) @@ -404,6 +437,7 @@ func TestInitAssetsComponentFail(t *testing.T) { defer or.cleanup(t) or.plugins.Database.Plugin = nil or.assets = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) @@ -414,6 +448,7 @@ func TestInitContractsComponentFail(t *testing.T) { defer or.cleanup(t) or.plugins.Database.Plugin = nil or.contracts = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) or.mmp.On("ConfigureContract", mock.Anything, mock.Anything).Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) @@ -425,6 +460,7 @@ func TestInitOperationsComponentFail(t *testing.T) { or.plugins.Database.Plugin = nil or.operations = nil or.txHelper = nil + or.mbi.On("StartNamespace", mock.Anything, "ns").Return(nil) err := or.initComponents(context.Background()) assert.Regexp(t, "FF10128", err) } @@ -467,13 +503,71 @@ func TestStartStopOk(t *testing.T) { or.mom.On("WaitStop").Return(nil) or.mem.On("WaitStop").Return(nil) or.mtw.On("Close").Return(nil) + or.mbi.On("StopNamespace", mock.Anything, "ns").Return(nil) + or.mti.On("StopNamespace", mock.Anything, "ns").Return(nil) err := or.Start() assert.NoError(t, err) or.WaitStop() or.WaitStop() // swallows dups + + or = newTestOrchestrator() + or.mdm.On("Start").Return(nil) + or.mba.On("Start").Return(nil) + or.mem.On("Start").Return(nil) + or.mbm.On("Start").Return(nil) + or.msd.On("Start").Return(nil) + or.mom.On("Start").Return(nil) + or.mtw.On("Start").Return() + or.mba.On("WaitStop").Return(nil) + or.mbm.On("WaitStop").Return(nil) + or.mdm.On("WaitStop").Return(nil) + or.msd.On("WaitStop").Return(nil) + or.mom.On("WaitStop").Return(nil) + or.mem.On("WaitStop").Return(nil) + or.mtw.On("Close").Return(nil) + or.mbi.On("StopNamespace", mock.Anything, "ns").Return(fmt.Errorf("pop")) + or.mti.On("StopNamespace", mock.Anything, "ns").Return(fmt.Errorf("pop")) + err = or.Start() + assert.NoError(t, err) + or.WaitStop() + or.WaitStop() // swallows dups } func TestPurge(t *testing.T) { + coreconfig.Reset() + or := newTestOrchestrator() + defer or.cleanup(t) + // Note additional testing of this happens in namespace manager + or.mdi.On("SetHandler", mock.Anything, mock.Anything).Return(nil) + or.mbi.On("SetHandler", mock.Anything, mock.Anything).Return(nil) + or.mbi.On("SetOperationHandler", mock.Anything, mock.Anything).Return(nil) + // or.mbi.On("StopNamespace", mock.Anything, mock.Anything).Return(nil) + or.mps.On("SetHandler", mock.Anything, mock.Anything).Return(nil) + or.mdx.On("SetHandler", mock.Anything, "Test1", mock.Anything).Return(nil) + or.mdx.On("SetOperationHandler", mock.Anything, mock.Anything).Return(nil) + or.mti.On("SetHandler", mock.Anything, mock.Anything).Return(nil) + or.mti.On("SetOperationHandler", mock.Anything, mock.Anything).Return(nil) + // or.mti.On("StopNamespace", mock.Anything, "ns").Return(nil) + Purge(context.Background(), or.namespace, or.plugins, "Test1") +} + +func TestPurgeBlockchainError(t *testing.T) { + coreconfig.Reset() + or := newTestOrchestrator() + defer or.cleanup(t) + // Note additional testing of this happens in namespace manager + or.mdi.On("SetHandler", mock.Anything, mock.Anything).Return(nil) + or.mbi.On("SetHandler", mock.Anything, mock.Anything).Return(nil) + or.mbi.On("SetOperationHandler", mock.Anything, mock.Anything).Return(nil) + or.mps.On("SetHandler", mock.Anything, mock.Anything).Return(nil) + or.mdx.On("SetHandler", mock.Anything, "Test1", mock.Anything).Return(nil) + or.mdx.On("SetOperationHandler", mock.Anything, mock.Anything).Return(nil) + or.mti.On("SetHandler", mock.Anything, mock.Anything).Return(nil) + or.mti.On("SetOperationHandler", mock.Anything, mock.Anything).Return(nil) + Purge(context.Background(), or.namespace, or.plugins, "Test1") +} + +func TestPurgeTokenError(t *testing.T) { coreconfig.Reset() or := newTestOrchestrator() defer or.cleanup(t) diff --git a/internal/tokens/fftokens/fftokens.go b/internal/tokens/fftokens/fftokens.go index 1d8132f2d..43153ec49 100644 --- a/internal/tokens/fftokens/fftokens.go +++ b/internal/tokens/fftokens/fftokens.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -19,7 +19,6 @@ package fftokens import ( "context" "encoding/json" - "fmt" "net/http" "strings" "sync" @@ -59,10 +58,10 @@ type FFTokens struct { callbacks callbacks configuredName string client *resty.Client - wsconn wsclient.WSClient + wsconn map[string]wsclient.WSClient + wsConfig *wsclient.WSConfig retry *retry.Retry - backgroundRetry *retry.Retry - backgroundStart bool + poolsToActivate map[string][]*core.TokenPool } type callbacks struct { @@ -155,6 +154,8 @@ const ( messageTokenBurn msgType = "token-burn" messageTokenTransfer msgType = "token-transfer" messageTokenApproval msgType = "token-approval" + messageStarted msgType = "started" + messageActivated msgType = "activated" ) type tokenData struct { @@ -165,6 +166,7 @@ type tokenData struct { } type createPool struct { + Namespace string `json:"namespace"` Type core.TokenType `json:"type"` RequestID string `json:"requestId"` Signer string `json:"signer"` @@ -175,12 +177,14 @@ type createPool struct { } type activatePool struct { + Namespace string `json:"namespace"` PoolData string `json:"poolData"` PoolLocator string `json:"poolLocator"` Config fftypes.JSONObject `json:"config"` } type deactivatePool struct { + Namespace string `json:"namespace"` PoolData string `json:"poolData"` PoolLocator string `json:"poolLocator"` Config fftypes.JSONObject `json:"config"` @@ -197,6 +201,7 @@ type checkInterface struct { } type mintTokens struct { + Namespace string `json:"namespace"` PoolLocator string `json:"poolLocator"` TokenIndex string `json:"tokenIndex,omitempty"` To string `json:"to"` @@ -210,6 +215,7 @@ type mintTokens struct { } type burnTokens struct { + Namespace string `json:"namespace"` PoolLocator string `json:"poolLocator"` TokenIndex string `json:"tokenIndex,omitempty"` From string `json:"from"` @@ -223,6 +229,7 @@ type burnTokens struct { type transferTokens struct { PoolLocator string `json:"poolLocator"` + Namespace string `json:"namespace"` TokenIndex string `json:"tokenIndex,omitempty"` From string `json:"from"` To string `json:"to"` @@ -235,6 +242,7 @@ type transferTokens struct { } type tokenApproval struct { + Namespace string `json:"namespace"` Signer string `json:"signer"` Operator string `json:"operator"` Approved bool `json:"approved"` @@ -250,6 +258,11 @@ type tokenError struct { Message string `json:"message,omitempty"` } +type wsAck struct { + core.WSActionBase + ID string `json:"id"` +} + func packPoolData(namespace string, id *fftypes.UUID) string { if id == nil { return namespace @@ -271,6 +284,10 @@ func (ft *FFTokens) Name() string { return "fftokens" } +func (ft *FFTokens) ConnectorName() string { + return ft.configuredName +} + func (ft *FFTokens) Init(ctx context.Context, cancelCtx context.CancelFunc, name string, config config.Section) (err error) { ft.ctx = log.WithLogField(ctx, "proto", "fftokens") ft.cancelCtx = cancelCtx @@ -286,7 +303,7 @@ func (ft *FFTokens) Init(ctx context.Context, cancelCtx context.CancelFunc, name return i18n.NewError(ctx, coremsgs.MsgMissingPluginConfig, "url", "tokens.fftokens") } - wsConfig, err := wsclient.GenerateConfig(ctx, config) + ft.wsConfig, err = wsclient.GenerateConfig(ctx, config) if err == nil { ft.client, err = ffresty.New(ft.ctx, config) } @@ -295,14 +312,11 @@ func (ft *FFTokens) Init(ctx context.Context, cancelCtx context.CancelFunc, name return err } - if wsConfig.WSKeyPath == "" { - wsConfig.WSKeyPath = "/api/ws" + if ft.wsConfig.WSKeyPath == "" { + ft.wsConfig.WSKeyPath = "/api/ws" } - ft.wsconn, err = wsclient.New(ctx, wsConfig, nil, nil) - if err != nil { - return err - } + ft.wsconn = make(map[string]wsclient.WSClient) ft.retry = &retry.Retry{ InitialDelay: config.GetDuration(FFTEventRetryInitialDelay), @@ -310,18 +324,50 @@ func (ft *FFTokens) Init(ctx context.Context, cancelCtx context.CancelFunc, name Factor: config.GetFloat64(FFTEventRetryFactor), } - ft.backgroundStart = config.GetBool(FFTBackgroundStart) + return nil +} - if ft.backgroundStart { - ft.backgroundRetry = &retry.Retry{ - InitialDelay: config.GetDuration(FFTBackgroundStartInitialDelay), - MaximumDelay: config.GetDuration(FFTBackgroundStartMaxDelay), - Factor: config.GetFloat64(FFTBackgroundStartFactor), +func (ft *FFTokens) StartNamespace(ctx context.Context, namespace string, activePools []*core.TokenPool) (err error) { + if ft.wsconn[namespace] == nil { + ft.wsconn[namespace], err = wsclient.New(ctx, ft.wsConfig, nil, nil) + if err != nil { + return err } - return nil } - go ft.eventLoop() + // Keep the list of pools we need to ensure are active + // The handleNamespaceStarted function will ensure pools are active after the namespace has finished starting + if ft.poolsToActivate == nil { + ft.poolsToActivate = make(map[string][]*core.TokenPool) + } + ft.poolsToActivate[namespace] = activePools + + err = ft.wsconn[namespace].Connect() + if err != nil { + return err + } + startCmd := core.WSStart{ + WSActionBase: core.WSActionBase{ + Type: core.WSClientActionStart, + }, + Namespace: namespace, + } + b, _ := json.Marshal(startCmd) + if err := ft.wsconn[namespace].Send(ctx, b); err != nil { + return err + } + + go ft.eventLoop(namespace) + + return nil +} + +func (ft *FFTokens) StopNamespace(ctx context.Context, namespace string) error { + wsconn, ok := ft.wsconn[namespace] + if ok { + wsconn.Close() + } + delete(ft.wsconn, namespace) return nil } @@ -346,27 +392,6 @@ func (ft *FFTokens) SetOperationHandler(namespace string, handler core.Operation } } -func (ft *FFTokens) backgroundStartLoop() { - _ = ft.backgroundRetry.Do(ft.ctx, fmt.Sprintf("Background start %s", ft.Name()), func(attempt int) (retry bool, err error) { - err = ft.wsconn.Connect() - if err != nil { - return true, err - } - - go ft.eventLoop() - - return false, nil - }) -} - -func (ft *FFTokens) Start() error { - if ft.backgroundStart { - go ft.backgroundStartLoop() - return nil - } - return ft.wsconn.Connect() -} - func (ft *FFTokens) Capabilities() *tokens.Capabilities { return ft.capabilities } @@ -428,6 +453,7 @@ func (ft *FFTokens) handleTokenPoolCreate(ctx context.Context, eventData fftypes tokenType := eventData.GetString("type") poolLocator := eventData.GetString("poolLocator") + alternateLocators := eventData.GetStringArray("alternateLocators") if tokenType == "" || poolLocator == "" { log.L(ctx).Errorf("TokenPool event is not valid - missing data: %+v", eventData) @@ -463,10 +489,11 @@ func (ft *FFTokens) handleTokenPoolCreate(ctx context.Context, eventData fftypes } pool := &tokens.TokenPool{ - ID: poolID, - Type: fftypes.FFEnum(tokenType), - PoolLocator: poolLocator, - PluginData: poolData, + ID: poolID, + Type: fftypes.FFEnum(tokenType), + PoolLocator: poolLocator, + AlternateLocators: alternateLocators, + PluginData: poolData, TX: core.TransactionRef{ ID: txData.TX, Type: txType, @@ -617,7 +644,7 @@ func (ft *FFTokens) handleTokenApproval(ctx context.Context, eventData fftypes.J return ft.callbacks.TokensApproved(ctx, namespace, approval) } -func (ft *FFTokens) handleMessage(ctx context.Context, msgBytes []byte) (retry bool, err error) { +func (ft *FFTokens) handleMessage(ctx context.Context, namespace string, msgBytes []byte) (retry bool, err error) { var msg *wsEvent if err = json.Unmarshal(msgBytes, &msg); err != nil { log.L(ctx).Errorf("Message cannot be parsed as JSON: %s\n%s", err, string(msgBytes)) @@ -629,7 +656,7 @@ func (ft *FFTokens) handleMessage(ctx context.Context, msgBytes []byte) (retry b ft.handleReceipt(ctx, msg.Data) case messageBatch: for _, msg := range msg.Data.GetObjectArray("events") { - if retry, err = ft.handleMessage(ctx, []byte(msg.String())); err != nil { + if retry, err = ft.handleMessage(ctx, namespace, []byte(msg.String())); err != nil { return retry, err } } @@ -643,6 +670,10 @@ func (ft *FFTokens) handleMessage(ctx context.Context, msgBytes []byte) (retry b err = ft.handleTokenTransfer(ctx, core.TokenTransferTypeTransfer, msg.Data) case messageTokenApproval: err = ft.handleTokenApproval(ctx, msg.Data) + case messageStarted: + err = ft.handleNamespaceStarted(ctx, msg.Data) + case messageActivated: + err = ft.handleTokenPoolActivated(ctx, msg.Data) default: log.L(ctx).Errorf("Message unexpected: %s", msg.Event) // do not set error here - we will never be able to process this message so log+swallow it. @@ -653,28 +684,51 @@ func (ft *FFTokens) handleMessage(ctx context.Context, msgBytes []byte) (retry b } if msg.Event != messageReceipt && msg.ID != "" { log.L(ctx).Debugf("Sending ack %s", msg.ID) - ack, _ := json.Marshal(fftypes.JSONObject{ - "event": "ack", - "data": fftypes.JSONObject{ - "id": msg.ID, + ack, _ := json.Marshal(wsAck{ + WSActionBase: core.WSActionBase{ + Type: core.WSClientActionAck, }, + ID: msg.ID, }) // Do not retry this - return false, ft.wsconn.Send(ctx, ack) + return false, ft.wsconn[namespace].Send(ctx, ack) } return false, nil } -func (ft *FFTokens) handleMessageRetry(ctx context.Context, msgBytes []byte) (err error) { +func (ft *FFTokens) handleMessageRetry(ctx context.Context, namespace string, msgBytes []byte) (err error) { eventCtx, done := context.WithCancel(ctx) defer done() return ft.retry.Do(eventCtx, "fftokens event", func(attempt int) (retry bool, err error) { - return ft.handleMessage(eventCtx, msgBytes) // We keep retrying on error until the context ends + return ft.handleMessage(eventCtx, namespace, msgBytes) // We keep retrying on error until the context ends }) } -func (ft *FFTokens) eventLoop() { - defer ft.wsconn.Close() +func (ft *FFTokens) handleNamespaceStarted(ctx context.Context, data fftypes.JSONObject) error { + // Make sure any pools that are marked as active in our DB are indeed active + namespace := data.GetString("namespace") + log.L(ctx).Debugf("Token connector '%s' started namespace '%s'. Ensuring all token pools active.", ft.Name(), namespace) + for _, pool := range ft.poolsToActivate[namespace] { + if _, err := ft.EnsureTokenPoolActive(ctx, pool); err == nil { + log.L(ctx).Debugf("Ensured token pool active '%s'", pool.ID) + } else { + // Log the error and continue trying to activate pools + // At this point we've already started + log.L(ctx).Errorf("Error ensuring token pool active '%s': %s", pool.ID, err.Error()) + } + + } + return nil +} + +func (ft *FFTokens) handleTokenPoolActivated(ctx context.Context, data fftypes.JSONObject) error { + // NOOP + return nil +} + +func (ft *FFTokens) eventLoop(namespace string) { + wsconn := ft.wsconn[namespace] + defer wsconn.Close() l := log.L(ft.ctx).WithField("role", "event-loop") ctx := log.WithLogger(ft.ctx, l) for { @@ -682,13 +736,13 @@ func (ft *FFTokens) eventLoop() { case <-ctx.Done(): l.Debugf("Event loop exiting (context cancelled)") return - case msgBytes, ok := <-ft.wsconn.Receive(): + case msgBytes, ok := <-wsconn.Receive(): if !ok { l.Debugf("Event loop exiting (receive channel closed). Terminating server!") ft.cancelCtx() return } - if err := ft.handleMessageRetry(ctx, msgBytes); err != nil { + if err := ft.handleMessageRetry(ctx, namespace, msgBytes); err != nil { l.Errorf("Event loop exiting (%s). Terminating server!", err) ft.cancelCtx() return @@ -728,6 +782,7 @@ func (ft *FFTokens) CreateTokenPool(ctx context.Context, nsOpID string, pool *co var errRes tokenError res, err := ft.client.R().SetContext(ctx). SetBody(&createPool{ + Namespace: pool.Namespace, Type: pool.Type, RequestID: nsOpID, Signer: pool.Key, @@ -754,10 +809,11 @@ func (ft *FFTokens) CreateTokenPool(ctx context.Context, nsOpID string, pool *co return core.OpPhasePending, nil } -func (ft *FFTokens) ActivateTokenPool(ctx context.Context, pool *core.TokenPool) (phase core.OpPhase, err error) { +func (ft *FFTokens) EnsureTokenPoolActive(ctx context.Context, pool *core.TokenPool) (res *resty.Response, err error) { var errRes tokenError - res, err := ft.client.R().SetContext(ctx). + res, err = ft.client.R().SetContext(ctx). SetBody(&activatePool{ + Namespace: pool.Namespace, PoolData: packPoolData(pool.Namespace, pool.ID), PoolLocator: pool.Locator, Config: pool.Config, @@ -765,7 +821,15 @@ func (ft *FFTokens) ActivateTokenPool(ctx context.Context, pool *core.TokenPool) SetError(&errRes). Post("/api/v1/activatepool") if err != nil || !res.IsSuccess() { - return core.OpPhaseInitializing, wrapError(ctx, &errRes, res, err) + return res, wrapError(ctx, &errRes, res, err) + } + return res, nil +} + +func (ft *FFTokens) ActivateTokenPool(ctx context.Context, pool *core.TokenPool) (phase core.OpPhase, err error) { + res, err := ft.EnsureTokenPoolActive(ctx, pool) + if err != nil || !res.IsSuccess() { + return core.OpPhaseInitializing, err } if res.StatusCode() == 200 { // HTTP 200: Activation was successful, and pool details are in response body @@ -790,6 +854,7 @@ func (ft *FFTokens) DeactivateTokenPool(ctx context.Context, pool *core.TokenPoo var errRes tokenError res, err := ft.client.R().SetContext(ctx). SetBody(&deactivatePool{ + Namespace: pool.Namespace, PoolData: pool.PluginData, PoolLocator: pool.Locator, Config: pool.Config, @@ -867,6 +932,7 @@ func (ft *FFTokens) MintTokens(ctx context.Context, nsOpID string, poolLocator s var errRes tokenError res, err := ft.client.R().SetContext(ctx). SetBody(&mintTokens{ + Namespace: mint.Namespace, PoolLocator: poolLocator, TokenIndex: mint.TokenIndex, To: mint.To, @@ -902,6 +968,7 @@ func (ft *FFTokens) BurnTokens(ctx context.Context, nsOpID string, poolLocator s var errRes tokenError res, err := ft.client.R().SetContext(ctx). SetBody(&burnTokens{ + Namespace: burn.Namespace, PoolLocator: poolLocator, TokenIndex: burn.TokenIndex, From: burn.From, @@ -936,6 +1003,7 @@ func (ft *FFTokens) TransferTokens(ctx context.Context, nsOpID string, poolLocat var errRes tokenError res, err := ft.client.R().SetContext(ctx). SetBody(&transferTokens{ + Namespace: transfer.Namespace, PoolLocator: poolLocator, TokenIndex: transfer.TokenIndex, From: transfer.From, @@ -971,6 +1039,7 @@ func (ft *FFTokens) TokensApproval(ctx context.Context, nsOpID string, poolLocat var errRes tokenError res, err := ft.client.R().SetContext(ctx). SetBody(&tokenApproval{ + Namespace: approval.Namespace, PoolLocator: poolLocator, Signer: approval.Key, Operator: approval.Operator, diff --git a/internal/tokens/fftokens/fftokens_test.go b/internal/tokens/fftokens/fftokens_test.go index da03d619a..c95651926 100644 --- a/internal/tokens/fftokens/fftokens_test.go +++ b/internal/tokens/fftokens/fftokens_test.go @@ -21,9 +21,8 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" - "net/http/httptest" "net/url" "testing" @@ -104,10 +103,13 @@ func TestInitBadURL(t *testing.T) { ctx, cancelCtx := context.WithCancel(context.Background()) err := h.Init(ctx, cancelCtx, "testtokens", ffTokensConfig) + assert.NoError(t, err) + + err = h.StartNamespace(ctx, "ns1", []*core.TokenPool{}) assert.Regexp(t, "FF00149", err) } -func TestInitBackgroundStart1(t *testing.T) { +func TestStartNamespaceConnectFail(t *testing.T) { coreconfig.Reset() h := &FFTokens{} h.InitConfig(ffTokensConfig) @@ -118,7 +120,22 @@ func TestInitBackgroundStart1(t *testing.T) { ctx, cancelCtx := context.WithCancel(context.Background()) err := h.Init(ctx, cancelCtx, "testtokens", ffTokensConfig) assert.NoError(t, err) - assert.NotNil(t, h.backgroundRetry) + + err = h.StartNamespace(ctx, "ns1", []*core.TokenPool{}) + assert.Error(t, err) +} + +func TestStopNamespace(t *testing.T) { + wsm := &wsmocks.WSClient{} + ctx, cancel := context.WithCancel(context.Background()) + wsm.On("Close").Return(nil) + cancel() + h := &FFTokens{ + ctx: ctx, + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, + } + h.StopNamespace(ctx, "ns1") + assert.Nil(t, h.wsconn["ns1"]) } func TestInitBadTLS(t *testing.T) { @@ -174,6 +191,7 @@ func TestCreateTokenPool(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "requestId": "ns1:" + opID.String(), "signer": "0x123", "type": "fungible", @@ -189,7 +207,7 @@ func TestCreateTokenPool(t *testing.T) { }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), + Body: io.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -300,7 +318,7 @@ func TestCreateTokenPoolSynchronous(t *testing.T) { assert.NoError(t, err) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ + Body: io.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ "type": "fungible", "poolLocator": "F1", "signer": "0x0", @@ -358,7 +376,7 @@ func TestCreateTokenPoolSynchronousBadResponse(t *testing.T) { assert.NoError(t, err) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte("bad"))), + Body: io.NopCloser(bytes.NewReader([]byte("bad"))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -391,13 +409,14 @@ func TestActivateTokenPool(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolData": "ns1", "poolLocator": "N1", "config": poolConfig, }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), + Body: io.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -450,13 +469,14 @@ func TestActivateTokenPoolSynchronous(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolData": "ns1", "poolLocator": "N1", "config": poolConfig, }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ + Body: io.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ "type": "fungible", "poolLocator": "F1", "signer": "0x0", @@ -499,13 +519,14 @@ func TestActivateTokenPoolSynchronousBadResponse(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolData": "ns1", "poolLocator": "N1", "config": poolConfig, }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte("bad"))), + Body: io.NopCloser(bytes.NewReader([]byte("bad"))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -545,6 +566,7 @@ func TestActivateTokenPoolNoContent(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolData": "ns1|" + pool.ID.String(), "poolLocator": "N1", "config": poolConfig, @@ -577,6 +599,7 @@ func TestDeactivateTokenPool(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolData": "ns1|pool1", "poolLocator": "N1", "config": nil, @@ -614,10 +637,11 @@ func TestMintTokens(t *testing.T) { defer done() mint := &core.TokenTransfer{ - LocalID: fftypes.NewUUID(), - To: "user1", - Key: "0x123", - Amount: *fftypes.NewFFBigInt(10), + Namespace: "ns1", + LocalID: fftypes.NewUUID(), + To: "user1", + Key: "0x123", + Amount: *fftypes.NewFFBigInt(10), TX: core.TransactionRef{ ID: fftypes.NewUUID(), Type: core.TransactionTypeTokenTransfer, @@ -636,6 +660,7 @@ func TestMintTokens(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolLocator": "123", "to": "user1", "amount": "10", @@ -652,7 +677,7 @@ func TestMintTokens(t *testing.T) { }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), + Body: io.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -670,10 +695,11 @@ func TestMintTokensWithInterface(t *testing.T) { defer done() mint := &core.TokenTransfer{ - LocalID: fftypes.NewUUID(), - To: "user1", - Key: "0x123", - Amount: *fftypes.NewFFBigInt(10), + Namespace: "ns1", + LocalID: fftypes.NewUUID(), + To: "user1", + Key: "0x123", + Amount: *fftypes.NewFFBigInt(10), TX: core.TransactionRef{ ID: fftypes.NewUUID(), Type: core.TransactionTypeTokenTransfer, @@ -693,6 +719,7 @@ func TestMintTokensWithInterface(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolLocator": "123", "to": "user1", "amount": "10", @@ -710,7 +737,7 @@ func TestMintTokensWithInterface(t *testing.T) { }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), + Body: io.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -728,10 +755,11 @@ func TestTokenApproval(t *testing.T) { defer done() approval := &core.TokenApproval{ - LocalID: fftypes.NewUUID(), - Operator: "0x02", - Key: "0x123", - Approved: true, + Namespace: "ns1", + LocalID: fftypes.NewUUID(), + Operator: "0x02", + Key: "0x123", + Approved: true, Config: fftypes.JSONObject{ "foo": "bar", }, @@ -749,6 +777,7 @@ func TestTokenApproval(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolLocator": "123", "operator": "0x02", "approved": true, @@ -764,7 +793,7 @@ func TestTokenApproval(t *testing.T) { }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), + Body: io.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -811,6 +840,7 @@ func TestBurnTokens(t *testing.T) { defer done() burn := &core.TokenTransfer{ + Namespace: "ns1", LocalID: fftypes.NewUUID(), TokenIndex: "1", From: "user1", @@ -833,6 +863,7 @@ func TestBurnTokens(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolLocator": "123", "tokenIndex": "1", "from": "user1", @@ -849,7 +880,7 @@ func TestBurnTokens(t *testing.T) { }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), + Body: io.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -882,6 +913,7 @@ func TestTransferTokens(t *testing.T) { defer done() transfer := &core.TokenTransfer{ + Namespace: "ns1", LocalID: fftypes.NewUUID(), TokenIndex: "1", From: "user1", @@ -905,6 +937,7 @@ func TestTransferTokens(t *testing.T) { err := json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, fftypes.JSONObject{ + "namespace": "ns1", "poolLocator": "123", "tokenIndex": "1", "from": "user1", @@ -922,7 +955,7 @@ func TestTransferTokens(t *testing.T) { }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), + Body: io.NopCloser(bytes.NewReader([]byte(`{"id":"1"}`))), Header: http.Header{ "Content-Type": []string{"application/json"}, }, @@ -954,160 +987,32 @@ func TestIgnoredEvents(t *testing.T) { h, toServer, fromServer, _, done := newTestFFTokens(t) defer done() - err := h.Start() + err := h.StartNamespace(context.Background(), "ns1", []*core.TokenPool{}) assert.NoError(t, err) fromServer <- `!}` // ignored fromServer <- `{}` // ignored fromServer <- `{"id":"1"}` // ignored but acked - msg := <-toServer - assert.Equal(t, `{"data":{"id":"1"},"event":"ack"}`, string(msg)) - fromServer <- fftypes.JSONObject{ - "id": "2", - "event": "receipt", - "data": fftypes.JSONObject{}, - }.String() -} - -func TestBackgroundStartFailWS(t *testing.T) { - h := &FFTokens{} - h.InitConfig(ffTokensConfig) - - // Create a listener and close it - to grab a port we know is not in use - badListener := httptest.NewServer(&http.ServeMux{}) - badURL := badListener.URL - badListener.Close() - - // Bad url for WS should fail and retry - ffTokensConfig.AddKnownKey(ffresty.HTTPConfigURL, badURL) - ffTokensConfig.Set(FFTBackgroundStart, true) - ffTokensConfig.Set(wsclient.WSConfigKeyInitialConnectAttempts, 1) - - ctx, cancelCtx := context.WithCancel(context.Background()) - err := h.Init(ctx, cancelCtx, "testtokens", ffTokensConfig) - assert.NoError(t, err) - assert.NotNil(t, h.backgroundRetry) - - capturedErr := make(chan error) - h.backgroundRetry = &retry.Retry{ - ErrCallback: func(err error) { - capturedErr <- err - }, - } - - err = h.Start() - assert.NoError(t, err) - - err = <-capturedErr - assert.Regexp(t, "FF00148", err) -} - -func TestReceiptEventsBackgroundStart(t *testing.T) { - - h, _, fromServer, _, done := newTestFFTokens(t) - defer done() - - ffTokensConfig.Set(FFTBackgroundStart, true) - - err := h.Init(h.ctx, h.cancelCtx, "testtokens", ffTokensConfig) - assert.NoError(t, err) - - // Reset the retry to be quicker - h.backgroundRetry = &retry.Retry{} - - err = h.Start() - assert.NoError(t, err) - - mcb := &coremocks.OperationCallbacks{} - h.SetOperationHandler("ns1", mcb) - opID := fftypes.NewUUID() - mockCalled := make(chan bool) - - // receipt: bad ID - passed through - mcb.On("OperationUpdate", mock.MatchedBy(func(update *core.OperationUpdate) bool { - return update.NamespacedOpID == "ns1:wrong" && - update.Status == core.OpStatusPending && - update.Plugin == "fftokens" - })).Return(nil).Once().Run(func(args mock.Arguments) { mockCalled <- true }) - fromServer <- fftypes.JSONObject{ - "id": "3", - "event": "receipt", - "data": fftypes.JSONObject{ - "headers": fftypes.JSONObject{ - "requestId": "ns1:wrong", // passed through to OperationUpdate to ignore - "type": "TransactionUpdate", - }, - }, - }.String() - <-mockCalled - - // receipt: success - mcb.On("OperationUpdate", mock.MatchedBy(func(update *core.OperationUpdate) bool { - return update.NamespacedOpID == "ns1:"+opID.String() && - update.Status == core.OpStatusSucceeded && - update.BlockchainTXID == "0xffffeeee" && - update.Plugin == "fftokens" - })).Return(nil).Once().Run(func(args mock.Arguments) { mockCalled <- true }) - fromServer <- fftypes.JSONObject{ - "id": "4", - "event": "receipt", - "data": fftypes.JSONObject{ - "headers": fftypes.JSONObject{ - "requestId": "ns1:" + opID.String(), - "type": "TransactionSuccess", - }, - "transactionHash": "0xffffeeee", - }, - }.String() - <-mockCalled + msg := <-toServer + assert.Equal(t, "{\"type\":\"start\",\"autoack\":null,\"namespace\":\"ns1\",\"name\":\"\",\"ephemeral\":false,\"filter\":{\"message\":{},\"transaction\":{},\"blockchainevent\":{}},\"options\":{}}", string(msg)) - // receipt: update - mcb.On("OperationUpdate", mock.MatchedBy(func(update *core.OperationUpdate) bool { - return update.NamespacedOpID == "ns1:"+opID.String() && - update.Status == core.OpStatusPending && - update.BlockchainTXID == "0xffffeeee" - })).Return(nil).Once().Run(func(args mock.Arguments) { mockCalled <- true }) - fromServer <- fftypes.JSONObject{ - "id": "5", - "event": "receipt", - "data": fftypes.JSONObject{ - "headers": fftypes.JSONObject{ - "requestId": "ns1:" + opID.String(), - "type": "TransactionUpdate", - }, - "transactionHash": "0xffffeeee", - }, - }.String() - <-mockCalled + msg = <-toServer + assert.JSONEq(t, `{"id":"1","type":"ack"}`, string(msg)) - // receipt: failure - mcb.On("OperationUpdate", mock.MatchedBy(func(update *core.OperationUpdate) bool { - return update.NamespacedOpID == "ns1:"+opID.String() && - update.Status == core.OpStatusFailed && - update.BlockchainTXID == "0xffffeeee" && - update.Plugin == "fftokens" - })).Return(nil).Once().Run(func(args mock.Arguments) { mockCalled <- true }) fromServer <- fftypes.JSONObject{ - "id": "5", - "event": "receipt", - "data": fftypes.JSONObject{ - "headers": fftypes.JSONObject{ - "requestId": "ns1:" + opID.String(), - "type": "TransactionFailed", - }, - "transactionHash": "0xffffeeee", - }, + "namespace": "ns1", + "id": "2", + "event": "receipt", + "data": fftypes.JSONObject{}, }.String() - <-mockCalled - - mcb.AssertExpectations(t) } + func TestReceiptEvents(t *testing.T) { h, _, fromServer, _, done := newTestFFTokens(t) defer done() - err := h.Start() + err := h.StartNamespace(context.Background(), "ns1", []*core.TokenPool{}) assert.NoError(t, err) mcb := &coremocks.OperationCallbacks{} @@ -1199,7 +1104,7 @@ func TestPoolEvents(t *testing.T) { h, toServer, fromServer, _, done := newTestFFTokens(t) defer done() - err := h.Start() + err := h.StartNamespace(context.Background(), "ns1", []*core.TokenPool{}) assert.NoError(t, err) mcb := &tokenmocks.Callbacks{} @@ -1211,8 +1116,12 @@ func TestPoolEvents(t *testing.T) { "id": "6", "event": "token-pool", }.String() + msg := <-toServer - assert.Equal(t, `{"data":{"id":"6"},"event":"ack"}`, string(msg)) + assert.Equal(t, "{\"type\":\"start\",\"autoack\":null,\"namespace\":\"ns1\",\"name\":\"\",\"ephemeral\":false,\"filter\":{\"message\":{},\"transaction\":{},\"blockchainevent\":{}},\"options\":{}}", string(msg)) + + msg = <-toServer + assert.JSONEq(t, `{"id":"6","type":"ack"}`, string(msg)) // token-pool: invalid uuid (success) mcb.On("TokenPoolCreated", mock.Anything, h, mock.MatchedBy(func(p *tokens.TokenPool) bool { @@ -1222,6 +1131,7 @@ func TestPoolEvents(t *testing.T) { "id": "7", "event": "token-pool", "data": fftypes.JSONObject{ + "namespace": "ns1", "id": "000000000010/000020/000030/000040", "type": "fungible", "poolLocator": "F1", @@ -1236,7 +1146,7 @@ func TestPoolEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"7"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"7","type":"ack"}`, string(msg)) // token-pool: success mcb.On("TokenPoolCreated", mock.Anything, h, mock.MatchedBy(func(p *tokens.TokenPool) bool { @@ -1261,7 +1171,7 @@ func TestPoolEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"8"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"8","type":"ack"}`, string(msg)) // token-pool: no handler fromServer <- fftypes.JSONObject{ @@ -1315,7 +1225,7 @@ func TestTransferEvents(t *testing.T) { h, toServer, fromServer, _, done := newTestFFTokens(t) defer done() - err := h.Start() + err := h.StartNamespace(context.Background(), "ns1", []*core.TokenPool{}) assert.NoError(t, err) mcb := &tokenmocks.Callbacks{} @@ -1327,8 +1237,12 @@ func TestTransferEvents(t *testing.T) { "id": "9", "event": "token-mint", }.String() + msg := <-toServer - assert.Equal(t, `{"data":{"id":"9"},"event":"ack"}`, string(msg)) + assert.Equal(t, "{\"type\":\"start\",\"autoack\":null,\"namespace\":\"ns1\",\"name\":\"\",\"ephemeral\":false,\"filter\":{\"message\":{},\"transaction\":{},\"blockchainevent\":{}},\"options\":{}}", string(msg)) + + msg = <-toServer + assert.JSONEq(t, `{"id":"9","type":"ack"}`, string(msg)) // token-mint: invalid amount fromServer <- fftypes.JSONObject{ @@ -1351,7 +1265,7 @@ func TestTransferEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"10"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"10","type":"ack"}`, string(msg)) // token-mint: success mcb.On("TokensTransferred", h, mock.MatchedBy(func(t *tokens.TokenTransfer) bool { @@ -1377,7 +1291,7 @@ func TestTransferEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"11"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"11","type":"ack"}`, string(msg)) // token-mint: invalid uuid (success) mcb.On("TokensTransferred", h, mock.MatchedBy(func(t *tokens.TokenTransfer) bool { @@ -1403,7 +1317,7 @@ func TestTransferEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"12"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"12","type":"ack"}`, string(msg)) // token-transfer: missing from fromServer <- fftypes.JSONObject{ @@ -1425,7 +1339,7 @@ func TestTransferEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"13"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"13","type":"ack"}`, string(msg)) // token-transfer: bad message hash (success) mcb.On("TokensTransferred", h, mock.MatchedBy(func(t *tokens.TokenTransfer) bool { @@ -1451,7 +1365,7 @@ func TestTransferEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"14"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"14","type":"ack"}`, string(msg)) // token-transfer: success messageID := fftypes.NewUUID() @@ -1478,7 +1392,7 @@ func TestTransferEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"15"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"15","type":"ack"}`, string(msg)) // token-burn: success mcb.On("TokensTransferred", h, mock.MatchedBy(func(t *tokens.TokenTransfer) bool { @@ -1504,7 +1418,7 @@ func TestTransferEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"16"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"16","type":"ack"}`, string(msg)) // token-transfer: callback fail mcb.On("TokensTransferred", h, mock.MatchedBy(func(t *tokens.TokenTransfer) bool { @@ -1535,7 +1449,7 @@ func TestApprovalEvents(t *testing.T) { h, toServer, fromServer, _, done := newTestFFTokens(t) defer done() - err := h.Start() + err := h.StartNamespace(context.Background(), "ns1", []*core.TokenPool{}) assert.NoError(t, err) mcb := &tokenmocks.Callbacks{} @@ -1570,8 +1484,12 @@ func TestApprovalEvents(t *testing.T) { }, }, }.String() + msg := <-toServer - assert.Equal(t, `{"data":{"id":"17"},"event":"ack"}`, string(msg)) + assert.Equal(t, "{\"type\":\"start\",\"autoack\":null,\"namespace\":\"ns1\",\"name\":\"\",\"ephemeral\":false,\"filter\":{\"message\":{},\"transaction\":{},\"blockchainevent\":{}},\"options\":{}}", string(msg)) + + msg = <-toServer + assert.JSONEq(t, `{"id":"17","type":"ack"}`, string(msg)) // token-approval: success (no data) mcb.On("TokensApproved", h, mock.MatchedBy(func(t *tokens.TokenApproval) bool { @@ -1597,7 +1515,7 @@ func TestApprovalEvents(t *testing.T) { }, }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"18"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"18","type":"ack"}`, string(msg)) // token-approval: missing data fromServer <- fftypes.JSONObject{ @@ -1605,7 +1523,7 @@ func TestApprovalEvents(t *testing.T) { "event": "token-approval", }.String() msg = <-toServer - assert.Equal(t, `{"data":{"id":"19"},"event":"ack"}`, string(msg)) + assert.JSONEq(t, `{"id":"19","type":"ack"}`, string(msg)) // token-approval: callback fail errProcessed := make(chan struct{}) @@ -1646,13 +1564,13 @@ func TestEventLoopReceiveClosed(t *testing.T) { h := &FFTokens{ ctx: context.Background(), cancelCtx: func() { called = true }, - wsconn: wsm, + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, } r := make(chan []byte) close(r) wsm.On("Close").Return() wsm.On("Receive").Return((<-chan []byte)(r)) - h.eventLoop() + h.eventLoop("ns1") assert.True(t, called) } @@ -1662,7 +1580,7 @@ func TestEventLoopSendClosed(t *testing.T) { h := &FFTokens{ ctx: context.Background(), cancelCtx: func() { called = true }, - wsconn: wsm, + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, retry: &retry.Retry{}, } r := make(chan []byte, 1) @@ -1670,23 +1588,36 @@ func TestEventLoopSendClosed(t *testing.T) { wsm.On("Close").Return() wsm.On("Receive").Return((<-chan []byte)(r)) wsm.On("Send", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) - h.eventLoop() + h.eventLoop("ns1") assert.True(t, called) } +func TestStartNamespaceSendClosed(t *testing.T) { + wsm := &wsmocks.WSClient{} + h := &FFTokens{ + ctx: context.Background(), + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, + retry: &retry.Retry{}, + } + wsm.On("Connect").Return(nil) + wsm.On("Send", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + err := h.StartNamespace(context.Background(), "ns1", []*core.TokenPool{}) + assert.Regexp(t, "pop", err) +} + func TestEventLoopClosedContext(t *testing.T) { wsm := &wsmocks.WSClient{} ctx, cancel := context.WithCancel(context.Background()) cancel() h := &FFTokens{ ctx: ctx, - wsconn: wsm, + wsconn: map[string]wsclient.WSClient{"ns1": wsm}, retry: &retry.Retry{}, } r := make(chan []byte, 1) wsm.On("Close").Return() wsm.On("Receive").Return((<-chan []byte)(r)) - h.eventLoop() // we're simply looking for it exiting + h.eventLoop("ns1") // we're simply looking for it exiting } func TestCallbacksWrongNamespace(t *testing.T) { @@ -1741,7 +1672,7 @@ func TestCheckInterfaceABI(t *testing.T) { }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ + Body: io.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ "approval": fftypes.JSONAny(`[]`), "burn": fftypes.JSONAny(`[]`), "mint": fftypes.JSONAny(`[]`), @@ -1807,7 +1738,7 @@ func TestCheckInterfaceFFI(t *testing.T) { }, body) res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ + Body: io.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ "approval": fftypes.JSONAny(`[]`), "burn": fftypes.JSONAny(`[]`), "mint": fftypes.JSONAny(`[]`), @@ -1859,7 +1790,7 @@ func TestCheckInterfaceFFIBadResponse(t *testing.T) { httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/checkinterface", httpURL), func(req *http.Request) (*http.Response, error) { res := &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ + Body: io.NopCloser(bytes.NewReader([]byte(fftypes.JSONObject{ "approval": map[bool]bool{true: false}, }.String()))), Header: http.Header{ @@ -1883,7 +1814,7 @@ func TestHandleEventRetryableFailure(t *testing.T) { ft.callbacks.handlers = map[string]tokens.Callbacks{ "ns1": mcb, } - retry, err := ft.handleMessage(context.Background(), []byte(`{ + retry, err := ft.handleMessage(context.Background(), "ns1", []byte(`{ "event": "batch", "data": { "events": [{ @@ -1926,3 +1857,43 @@ func TestErrorWrappingBodyErr(t *testing.T) { assert.True(t, ok) assert.True(t, errInterface.IsConflictError()) } + +func TestHandleNamespaceStartedEnsureActive(t *testing.T) { + h, _, _, httpURL, done := newTestFFTokens(t) + defer done() + h.poolsToActivate = map[string][]*core.TokenPool{ + "ns1": {{Active: true}}, + } + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/activatepool", httpURL), + httpmock.NewJsonResponderOrPanic(200, fftypes.JSONObject{})) + + _, err := h.handleMessage(context.Background(), "ns1", []byte(`{"event":"started","data":{"namespace": "ns1"}}`)) + assert.NoError(t, err) +} + +func TestHandleNamespaceStartedEnsureActiveError(t *testing.T) { + h, _, _, httpURL, done := newTestFFTokens(t) + defer done() + h.poolsToActivate = map[string][]*core.TokenPool{ + "ns1": {{Active: true}}, + } + httpmock.RegisterResponder("POST", fmt.Sprintf("%s/api/v1/activatepool", httpURL), + httpmock.NewJsonResponderOrPanic(500, fftypes.JSONObject{})) + + _, err := h.handleMessage(context.Background(), "ns1", []byte(`{"event":"started","data":{"namespace": "ns1"}}`)) + assert.NoError(t, err) +} + +func TestHandlePoolActivated(t *testing.T) { + h, _, _, _, done := newTestFFTokens(t) + defer done() + _, err := h.handleMessage(context.Background(), "ns1", []byte(`{"event":"activated","dat":{"namespace": "ns1"}}`)) + assert.NoError(t, err) +} + +func TestConnectorName(t *testing.T) { + h := &FFTokens{ + configuredName: "bob", + } + assert.Equal(t, h.ConnectorName(), "bob") +} diff --git a/manifest.json b/manifest.json index 3d50771f2..4138ff475 100644 --- a/manifest.json +++ b/manifest.json @@ -26,13 +26,13 @@ }, "tokens-erc1155": { "image": "ghcr.io/hyperledger/firefly-tokens-erc1155", - "tag": "v1.2.4", - "sha": "cd65eab2e5836b52dfed29a517049e21e2bacd534203b191cee7df42544847f7" + "tag": "v1.3.0-rc.1", + "sha": "028defcd14372231b878e3a214cead685f3c7577b4d2541e56f1e4111e777272" }, "tokens-erc20-erc721": { "image": "ghcr.io/hyperledger/firefly-tokens-erc20-erc721", - "tag": "v1.2.6", - "sha": "4e902d1d9f115c4dc608f5d68c4bd9920e118c7eec8c99a2abdb3aa9de7e7368" + "tag": "v1.3.0-rc.1", + "sha": "9a5efe3e3f4dd15383a1db22387a91e01b0d2ba5aae6d8e09d11a01883ac37c1" }, "signer": { "image": "ghcr.io/hyperledger/firefly-signer", @@ -59,6 +59,6 @@ "release": "v1.2.0" }, "cli": { - "tag": "c71842f6d2df5167859d5ae87caf14a4e16c5ee1" + "tag": "v1.3.0-rc.2" } } diff --git a/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go index 262b2ac67..dcbf9bf53 100644 --- a/mocks/assetmocks/manager.go +++ b/mocks/assetmocks/manager.go @@ -663,6 +663,20 @@ func (_m *Manager) RunOperation(ctx context.Context, op *core.PreparedOperation) return r0, r1, r2 } +// Start provides a mock function with given fields: ctx +func (_m *Manager) Start(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // TokenApproval provides a mock function with given fields: ctx, approval, waitConfirm func (_m *Manager) TokenApproval(ctx context.Context, approval *core.TokenApprovalInput, waitConfirm bool) (*core.TokenApproval, error) { ret := _m.Called(ctx, approval, waitConfirm) diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index 1f0802259..a39c66d24 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -239,9 +239,9 @@ func (_m *Plugin) GetAndConvertDeprecatedContractConfig(ctx context.Context) (*f return r0, r1, r2 } -// GetContractListenerStatus provides a mock function with given fields: ctx, subID, okNotFound -func (_m *Plugin) GetContractListenerStatus(ctx context.Context, subID string, okNotFound bool) (bool, interface{}, error) { - ret := _m.Called(ctx, subID, okNotFound) +// GetContractListenerStatus provides a mock function with given fields: ctx, namespace, subID, okNotFound +func (_m *Plugin) GetContractListenerStatus(ctx context.Context, namespace string, subID string, okNotFound bool) (bool, interface{}, error) { + ret := _m.Called(ctx, namespace, subID, okNotFound) if len(ret) == 0 { panic("no return value specified for GetContractListenerStatus") @@ -250,25 +250,25 @@ func (_m *Plugin) GetContractListenerStatus(ctx context.Context, subID string, o var r0 bool var r1 interface{} var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, bool) (bool, interface{}, error)); ok { - return rf(ctx, subID, okNotFound) + if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) (bool, interface{}, error)); ok { + return rf(ctx, namespace, subID, okNotFound) } - if rf, ok := ret.Get(0).(func(context.Context, string, bool) bool); ok { - r0 = rf(ctx, subID, okNotFound) + if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) bool); ok { + r0 = rf(ctx, namespace, subID, okNotFound) } else { r0 = ret.Get(0).(bool) } - if rf, ok := ret.Get(1).(func(context.Context, string, bool) interface{}); ok { - r1 = rf(ctx, subID, okNotFound) + if rf, ok := ret.Get(1).(func(context.Context, string, string, bool) interface{}); ok { + r1 = rf(ctx, namespace, subID, okNotFound) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(interface{}) } } - if rf, ok := ret.Get(2).(func(context.Context, string, bool) error); ok { - r2 = rf(ctx, subID, okNotFound) + if rf, ok := ret.Get(2).(func(context.Context, string, string, bool) error); ok { + r2 = rf(ctx, namespace, subID, okNotFound) } else { r2 = ret.Error(2) } @@ -566,17 +566,31 @@ func (_m *Plugin) SetOperationHandler(namespace string, handler core.OperationCa _m.Called(namespace, handler) } -// Start provides a mock function with given fields: -func (_m *Plugin) Start() error { - ret := _m.Called() +// StartNamespace provides a mock function with given fields: ctx, namespace +func (_m *Plugin) StartNamespace(ctx context.Context, namespace string) error { + ret := _m.Called(ctx, namespace) if len(ret) == 0 { panic("no return value specified for Start") } var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, namespace) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// StopNamespace provides a mock function with given fields: ctx, namespace +func (_m *Plugin) StopNamespace(ctx context.Context, namespace string) error { + ret := _m.Called(ctx, namespace) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, namespace) } else { r0 = ret.Error(0) } diff --git a/mocks/tokenmocks/plugin.go b/mocks/tokenmocks/plugin.go index 03a5e09cf..c8a1d6759 100644 --- a/mocks/tokenmocks/plugin.go +++ b/mocks/tokenmocks/plugin.go @@ -117,6 +117,20 @@ func (_m *Plugin) CheckInterface(ctx context.Context, pool *core.TokenPool, meth return r0, r1 } +// ConnectorName provides a mock function with given fields: +func (_m *Plugin) ConnectorName() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // CreateTokenPool provides a mock function with given fields: ctx, nsOpID, pool func (_m *Plugin) CreateTokenPool(ctx context.Context, nsOpID string, pool *core.TokenPool) (core.OpPhase, error) { ret := _m.Called(ctx, nsOpID, pool) @@ -232,17 +246,31 @@ func (_m *Plugin) SetOperationHandler(namespace string, handler core.OperationCa _m.Called(namespace, handler) } -// Start provides a mock function with given fields: -func (_m *Plugin) Start() error { - ret := _m.Called() +// StartNamespace provides a mock function with given fields: ctx, namespace, tokenPools +func (_m *Plugin) StartNamespace(ctx context.Context, namespace string, tokenPools []*core.TokenPool) error { + ret := _m.Called(ctx, namespace, tokenPools) if len(ret) == 0 { panic("no return value specified for Start") } var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() + if rf, ok := ret.Get(0).(func(context.Context, string, []*core.TokenPool) error); ok { + r0 = rf(ctx, namespace, tokenPools) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// StopNamespace provides a mock function with given fields: ctx, namespace +func (_m *Plugin) StopNamespace(ctx context.Context, namespace string) error { + ret := _m.Called(ctx, namespace) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, namespace) } else { r0 = ret.Error(0) } diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index cd576772a..33bdd42a0 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -45,6 +45,12 @@ type Plugin interface { // Init initializes the plugin, with configuration Init(ctx context.Context, cancelCtx context.CancelFunc, config config.Section, metrics metrics.Manager, cacheManager cache.Manager) error + // StartNamespace starts a specific namespace within the plugin + StartNamespace(ctx context.Context, namespace string) error + + // StopNamespace removes a namespace from use within the plugin + StopNamespace(ctx context.Context, namespace string) error + // SetHandler registers a handler to receive callbacks // Plugin will attempt (but is not guaranteed) to deliver events only for the given namespace SetHandler(namespace string, handler Callbacks) @@ -53,9 +59,6 @@ type Plugin interface { // If namespace is set, plugin will attempt to deliver only events for that namespace SetOperationHandler(namespace string, handler core.OperationCallbacks) - // Blockchain interface must not deliver any events until start is called - Start() error - // Capabilities returns capabilities - not called until after Init Capabilities() *Capabilities @@ -100,7 +103,7 @@ type Plugin interface { DeleteContractListener(ctx context.Context, subscription *core.ContractListener, okNotFound bool) error // GetContractListenerStatus gets the status of a contract listener from the backend connector. Returns false if not found - GetContractListenerStatus(ctx context.Context, subID string, okNotFound bool) (bool, interface{}, error) + GetContractListenerStatus(ctx context.Context, namespace, subID string, okNotFound bool) (bool, interface{}, error) // GetFFIParamValidator returns a blockchain-plugin-specific validator for FFIParams and their JSON Schema GetFFIParamValidator(ctx context.Context) (fftypes.FFIParamValidator, error) diff --git a/pkg/tokens/plugin.go b/pkg/tokens/plugin.go index 328f62c20..8b814d8ef 100644 --- a/pkg/tokens/plugin.go +++ b/pkg/tokens/plugin.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -43,12 +43,18 @@ type Plugin interface { // If namespace is set, plugin will attempt to deliver only events for that namespace SetOperationHandler(namespace string, handler core.OperationCallbacks) - // Token interface must not deliver any events until start is called - Start() error + // StartNamespace starts a specific namespace within the plugin + StartNamespace(ctx context.Context, namespace string, tokenPools []*core.TokenPool) error + + // StopNamespace removes a namespace from use within the plugin + StopNamespace(ctx context.Context, namespace string) error // Capabilities returns capabilities - not called until after Init Capabilities() *Capabilities + // ConnectorName returns the configured connector name (plugin instance) + ConnectorName() string + // CreateTokenPool creates a new (fungible or non-fungible) pool of tokens CreateTokenPool(ctx context.Context, nsOpID string, pool *core.TokenPool) (phase core.OpPhase, err error) @@ -116,6 +122,11 @@ type TokenPool struct { // PoolLocator is the identifier assigned to this pool by the token connector (includes the contract address or other location info) PoolLocator string + // AlternateLocators is a list of PoolLocators by which a previous version of the connector may have referred to this pool + // It will only be set on a TokenPoolCreated event and FireFly can use it to match and update an existing pool that is now + // referred to by a new locator + AlternateLocators []string + // TX is the FireFly-assigned information to correlate this to a transaction (optional) TX core.TransactionRef