diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a4a83d9..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 07831c8..fc71c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ docker-compose.yaml docs/node_modules docs/web_deploy vendor/ +airdrop-svc +.DS_Store \ No newline at end of file diff --git a/config.yaml b/config.yaml index c5f3b85..c895ccc 100644 --- a/config.yaml +++ b/config.yaml @@ -9,8 +9,15 @@ listener: addr: localhost:8000 broadcaster: - addr: broadcaster - sender_account: "rarimo15hcd6tv7pe8hk2re7hu0zg0aphqdm2dtjrs0ds" + airdrop_amount: 100stake + cosmos_rpc: rpc_url + chain_id: chain_id + sender_private_key: priv_key + query_limit: 10 -airdrop: - amount: 100000 # urmo +verifier: + verification_keys_paths: + sha1: "./verification_key.json" + sha256: "./verification_key.json" + allowed_age: 18 + allowed_citizenships: ["UKR"] diff --git a/docs/spec/components/responses/invalidAuth.yaml b/docs/spec/components/responses/invalidAuth.yaml deleted file mode 100644 index 07a81f5..0000000 --- a/docs/spec/components/responses/invalidAuth.yaml +++ /dev/null @@ -1,5 +0,0 @@ -description: You must provide a valid authorization params. -content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/Errors' \ No newline at end of file diff --git a/docs/spec/components/schemas/Airdrop.yaml b/docs/spec/components/schemas/Airdrop.yaml new file mode 100644 index 0000000..4599f1e --- /dev/null +++ b/docs/spec/components/schemas/Airdrop.yaml @@ -0,0 +1,42 @@ +allOf: + - $ref: '#/components/schemas/AirdropKey' + - type: object + required: + - attributes + properties: + attributes: + type: object + required: + - address + - status + - amount + - tx_hash + - created_at + - updated_at + properties: + address: + type: string + description: Destination address for the airdrop + example: "rarimo1qlyq3ej7j7rrkw6sluz658pzne88ymf66vjcap" + status: + type: string + description: Status of the airdrop transaction + enum: [ pending, completed ] + created_at: + type: string + format: time.Time + description: RFC3339 UTC timestamp of the airdrop creation + example: "2021-09-01T00:00:00Z" + updated_at: + type: string + format: time.Time + description: RFC3339 UTC timestamp of the airdrop successful tx + example: "2021-09-01T00:00:00Z" + amount: + type: string + description: Amount of airdropped coins + example: "100stake" + tx_hash: + type: string + description: Hash of the airdrop transaction + example: "F1CC0E80E151A67F75E41F2CDBF07920C29C9A3CDB6131B2A23A7C9D1964AD0B" diff --git a/docs/spec/components/schemas/AirdropKey.yaml b/docs/spec/components/schemas/AirdropKey.yaml new file mode 100644 index 0000000..9ec9254 --- /dev/null +++ b/docs/spec/components/schemas/AirdropKey.yaml @@ -0,0 +1,12 @@ +type: object +required: + - id + - type +properties: + id: + type: string + description: User nullifier + example: "0x04a32216f2425dc7343031352de3d62a7b0d3b4bf7a66d6c8c2aa8c9f4f2632b" + type: + type: string + enum: [ airdrop ] diff --git a/docs/spec/components/schemas/CreateAirdrop.yaml b/docs/spec/components/schemas/CreateAirdrop.yaml index e2da382..c77abf4 100644 --- a/docs/spec/components/schemas/CreateAirdrop.yaml +++ b/docs/spec/components/schemas/CreateAirdrop.yaml @@ -9,13 +9,19 @@ allOf: type: object required: - address - - proof + - algorithm + - zk_proof properties: address: type: string description: Destination address for the airdrop example: "rarimo1qlyq3ej7j7rrkw6sluz658pzne88ymf66vjcap" - proof: + algorithm: type: string - description: Placeholder for the proof + description: Signing algorithm used in proof. The value from passport document SOD is assumed. + example: "sha256_ecdsa" + zk_proof: + type: string + format: types.ZKProof + description: ZK-proof of the passport data example: "{}" diff --git a/docs/spec/components/schemas/CreateAirdropKey.yaml b/docs/spec/components/schemas/CreateAirdropKey.yaml index 2737204..637132a 100644 --- a/docs/spec/components/schemas/CreateAirdropKey.yaml +++ b/docs/spec/components/schemas/CreateAirdropKey.yaml @@ -1,12 +1,7 @@ type: object required: - - id - type properties: - id: - type: string - description: Nullifier - example: "0x04a32216f2425dc7343031352de3d62a7b0d3b4bf7a66d6c8c2aa8c9f4f2632b" type: type: string enum: [ create_airdrop ] diff --git a/docs/spec/paths/integrations@airdrop-svc@airdrops.yaml b/docs/spec/paths/integrations@airdrop-svc@airdrops.yaml index 3e551a6..758f973 100644 --- a/docs/spec/paths/integrations@airdrop-svc@airdrops.yaml +++ b/docs/spec/paths/integrations@airdrop-svc@airdrops.yaml @@ -15,12 +15,19 @@ post: data: $ref: '#/components/schemas/CreateAirdrop' responses: - 204: - description: No content - 401: - $ref: '#/components/responses/invalidAuth' - 404: - $ref: '#/components/responses/notFound' + 201: + description: Airdrop was created, transaction was queued + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Airdrop' + 400: + $ref: '#/components/responses/invalidParameter' 409: description: Airdrop was already done content: diff --git a/docs/spec/paths/integrations@airdrop-svc@airdrops@{id}.yaml b/docs/spec/paths/integrations@airdrop-svc@airdrops@{id}.yaml new file mode 100644 index 0000000..ef42277 --- /dev/null +++ b/docs/spec/paths/integrations@airdrop-svc@airdrops@{id}.yaml @@ -0,0 +1,37 @@ +get: + tags: + - Airdrop + summary: Get an airdrop + description: Get an airdrop for unique user. + operationId: createAirdrop + parameters: + - in: path + name: id + description: User nullifier + required: true + schema: + type: string + example: "0x04a32216f2425dc7343031352de3d62a7b0d3b4bf7a66d6c8c2aa8c9f4f2632b" + responses: + 200: + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Airdrop' + 400: + $ref: '#/components/responses/invalidParameter' + 404: + $ref: '#/components/responses/notFound' + 409: + description: Airdrop was already done + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 500: + $ref: '#/components/responses/internalError' diff --git a/go.mod b/go.mod index be5561c..3043569 100644 --- a/go.mod +++ b/go.mod @@ -6,22 +6,20 @@ require ( github.com/Masterminds/squirrel v1.4.0 github.com/alecthomas/kingpin v2.2.6+incompatible github.com/cosmos/cosmos-sdk v0.46.12 + github.com/decred/dcrd/bech32 v1.1.3 github.com/ethereum/go-ethereum v1.13.11 github.com/go-chi/chi v4.1.2+incompatible - github.com/go-co-op/gocron/v2 v2.2.2 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 - github.com/google/jsonapi v1.0.0 - github.com/iden3/go-iden3-core/v2 v2.0.4 - github.com/rarimo/auth-svc v1.0.0-rc2.0.20240311143312-de1e2258f175 - github.com/rarimo/saver-grpc-lib v1.0.0 + github.com/iden3/go-rapidsnark/types v0.0.3 + github.com/iden3/go-rapidsnark/verifier v0.0.5 + github.com/rarimo/rarimo-core v0.0.0-20231004143803-6b209428ecbf github.com/rubenv/sql-migrate v1.6.1 gitlab.com/distributed_lab/ape v1.7.1 gitlab.com/distributed_lab/figure/v3 v3.1.3 - gitlab.com/distributed_lab/json-api-connector v0.2.7 gitlab.com/distributed_lab/kit v1.11.2 gitlab.com/distributed_lab/logan v3.8.1+incompatible gitlab.com/distributed_lab/running v0.0.0-20200706131153-4af0e83eb96c - gitlab.com/distributed_lab/urlval/v4 v4.0.3 + google.golang.org/grpc v1.59.0 ) require ( @@ -64,7 +62,6 @@ require ( github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect - github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/getsentry/raven-go v0.2.0 // indirect github.com/getsentry/sentry-go v0.26.0 // indirect @@ -75,11 +72,11 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/protobuf v1.3.3 // indirect - github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/glog v1.1.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/jsonapi v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect @@ -96,7 +93,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/jmoiron/sqlx v1.2.0 // indirect - github.com/jonboulle/clockwork v0.4.0 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -106,9 +102,9 @@ require ( github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect @@ -119,9 +115,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.39.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/rarimo/broadcaster-svc v1.0.2 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect @@ -162,7 +156,6 @@ require ( google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect - google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index de20375..2a99728 100644 --- a/go.sum +++ b/go.sum @@ -616,8 +616,6 @@ github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91 github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= -github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= 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= @@ -694,6 +692,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/bech32 v1.1.3 h1:EeipVC1dO4zkjTjyqvrWt6JT2Ajr1EHZt+BAmWN864s= +github.com/decred/dcrd/bech32 v1.1.3/go.mod h1:jliqHZmCbVfT06Lh1mQywEKFVidRclbBJIUmwdoKhu0= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -735,7 +735,6 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -746,7 +745,6 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -769,8 +767,6 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/ github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-co-op/gocron/v2 v2.2.2 h1:XgQIiocDrdSX/B2AICR0FWk4ZeyJzeK5LVmLRoou3F0= -github.com/go-co-op/gocron/v2 v2.2.2/go.mod h1:igssOwzZkfcnu3m2kwnCf/mYj4SmhP9ecSgmYjCOHkk= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= @@ -821,8 +817,6 @@ github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -1007,10 +1001,12 @@ github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFck github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/iden3/go-iden3-core/v2 v2.0.4 h1:ggzC2zgOWgJAAcuG9X8bQG1r4gAoHZWqY7aLV8b1qgc= -github.com/iden3/go-iden3-core/v2 v2.0.4/go.mod h1:L9PxhWPvoS9qTb3inEkZBm1RpjHBt+VTwvxssdzbAdw= github.com/iden3/go-iden3-crypto v0.0.15 h1:4MJYlrot1l31Fzlo2sF56u7EVFeHHJkxGXXZCtESgK4= github.com/iden3/go-iden3-crypto v0.0.15/go.mod h1:dLpM4vEPJ3nDHzhWFXDjzkn1qHoBeOT/3UEhXsEsP3E= +github.com/iden3/go-rapidsnark/types v0.0.3 h1:f0s1Qdut1qHe1O67+m+xUVRBPwSXnq5j0xSrBi0jqM4= +github.com/iden3/go-rapidsnark/types v0.0.3/go.mod h1:ApgcaUxKIgSRA6fAeFxK7p+lgXXfG4oA2HN5DhFlfF4= +github.com/iden3/go-rapidsnark/verifier v0.0.5 h1:J7y0ovrEjDQoWtZmlrp4tgGng1A9faMeYsQH4igAEqA= +github.com/iden3/go-rapidsnark/verifier v0.0.5/go.mod h1:KgL3Yr9NehlFDI4EIWVLE3UDUi8ulyjbp7HcXSBfiGI= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= @@ -1030,8 +1026,6 @@ github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -1145,8 +1139,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -1167,8 +1159,9 @@ github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= +github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -1217,14 +1210,10 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= -github.com/rarimo/auth-svc v1.0.0-rc2.0.20240311143312-de1e2258f175 h1:LWfw+633hf9J0unWMA528CEf+taYdzhhoqJVFj3po6Y= -github.com/rarimo/auth-svc v1.0.0-rc2.0.20240311143312-de1e2258f175/go.mod h1:XtPIuJABJ3QNmhcpmJRlQrxuagPfpIWwFbY0j5SakFg= -github.com/rarimo/broadcaster-svc v1.0.2 h1:ExQcjjWCRP5+POLDlZHrTD1ffUsBH+Dgv5FAgcP3BXc= -github.com/rarimo/broadcaster-svc v1.0.2/go.mod h1:lYIHy+X4IqQt4eBdtMN/V352H3EV0/gO8G+32SFwUWI= github.com/rarimo/cosmos-sdk v0.46.7 h1:jU2PiWzc+19SF02cXM0O0puKPeH1C6Q6t2lzJ9s1ejc= github.com/rarimo/cosmos-sdk v0.46.7/go.mod h1:fqKqz39U5IlEFb4nbQ72951myztsDzFKKDtffYJ63nk= -github.com/rarimo/saver-grpc-lib v1.0.0 h1:MGUVjYg7unmodYczVsLqlqZNkT4CIgKqdo6aQtL1qdE= -github.com/rarimo/saver-grpc-lib v1.0.0/go.mod h1:DpugWK5B7Hi0bdC3MPe/9FD2zCxaRwsyykdwxtF1Zgg= +github.com/rarimo/rarimo-core v0.0.0-20231004143803-6b209428ecbf h1:NvYhOErW0d7ohn2YzGxQYKssrgVrKOvjrKL1OBQgCB4= +github.com/rarimo/rarimo-core v0.0.0-20231004143803-6b209428ecbf/go.mod h1:Onkd0EJP94hw4dT/2KH7QXRwDG4eIGeaMffSjA1i6/s= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/regen-network/cosmos-proto v0.3.1 h1:rV7iM4SSFAagvy8RiyhiACbWEGotmqzywPxOvwMdxcg= @@ -1234,8 +1223,6 @@ github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -1282,7 +1269,6 @@ github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -1328,6 +1314,14 @@ github.com/tendermint/tendermint v0.34.24 h1:879MKKJWYYPJEMMKME+DWUTY4V9f/FBpnZD github.com/tendermint/tendermint v0.34.24/go.mod h1:rXVrl4OYzmIa1I91av3iLv2HS0fGSiucyW9J4aMTpKI= github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8= github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -1371,7 +1365,6 @@ github.com/zondax/hid v0.9.1 h1:gQe66rtmyZ8VeGFcOpbuH3r7erYtNEAezCAYu8LdkJo= github.com/zondax/hid v0.9.1/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.1 h1:Pip65OOl4iJ84WTpA4BKChvOufMhhbxED3BaihoZN4c= github.com/zondax/ledger-go v0.14.1/go.mod h1:fZ3Dqg6qcdXWSOJFKMG8GCTnD7slO/RL2feOQv8K320= -gitlab.com/distributed_lab/ape v1.6.1/go.mod h1:Qy9Y2arL0hmZIpVpctGEFhdrVsjWtyVJ5G+bZWcFT4s= gitlab.com/distributed_lab/ape v1.7.1 h1:LpTmZgG7Lvx6ulopQbH2aWI3s8ey9FsKVjbic3ZQIy4= gitlab.com/distributed_lab/ape v1.7.1/go.mod h1:Qy9Y2arL0hmZIpVpctGEFhdrVsjWtyVJ5G+bZWcFT4s= gitlab.com/distributed_lab/figure v2.1.0+incompatible/go.mod h1:tk+aPBohT49MGPLy5+eVbE1HpD/CaC5drBHfVpRI8eE= @@ -1379,12 +1372,9 @@ gitlab.com/distributed_lab/figure v2.1.2+incompatible h1:xO1KCYPK9KFx6OUBOaJ62d8 gitlab.com/distributed_lab/figure v2.1.2+incompatible/go.mod h1:tk+aPBohT49MGPLy5+eVbE1HpD/CaC5drBHfVpRI8eE= gitlab.com/distributed_lab/figure/v3 v3.1.3 h1:gCHplT1Ih8B1s4eYTeAhRZyto3gIWoUCUj3yYfNM4r8= gitlab.com/distributed_lab/figure/v3 v3.1.3/go.mod h1:gYbCEdQBQCVEg+ap0zrpjY56BU95k9H8ELebL1ChONo= -gitlab.com/distributed_lab/json-api-connector v0.2.7 h1:cwKDOxY/WLNFUJqpj90gGwnrdOZctQPD6RiTEJ7rNw4= -gitlab.com/distributed_lab/json-api-connector v0.2.7/go.mod h1:/jNqcDl22LxF06EOYsU8DvLpYwB5okFvesDotsj4ClA= gitlab.com/distributed_lab/kit v1.11.2 h1:3GYAVe/ih5fvFuM/44zIorv9mUyD3JBQe/5v+GL7x+k= gitlab.com/distributed_lab/kit v1.11.2/go.mod h1:MZj5Vb71YBWJ2wLAb9fDvlCYKewmNDNVWjAiERwgbdA= gitlab.com/distributed_lab/logan v3.7.2+incompatible/go.mod h1:25oL/FPFXmyYzWeA6vahMvnFJV8P7mOx0jZhRP7nhlc= -gitlab.com/distributed_lab/logan v3.8.0+incompatible/go.mod h1:25oL/FPFXmyYzWeA6vahMvnFJV8P7mOx0jZhRP7nhlc= gitlab.com/distributed_lab/logan v3.8.1+incompatible h1:bYiP3P0AA0cpAL/fyOYWGq1aiKw16vZFoJz+nwbqdvU= gitlab.com/distributed_lab/logan v3.8.1+incompatible/go.mod h1:25oL/FPFXmyYzWeA6vahMvnFJV8P7mOx0jZhRP7nhlc= gitlab.com/distributed_lab/lorem v0.2.0/go.mod h1:wkzrGoB1L/yUBu56SfoJ/vNiPqiHZcg75AnBkWNcjhQ= @@ -1392,8 +1382,6 @@ gitlab.com/distributed_lab/lorem v0.2.1 h1:A1QoiEDRN3vlPrwsXJmPlENanQwu3FxpDl5vE gitlab.com/distributed_lab/lorem v0.2.1/go.mod h1:wkzrGoB1L/yUBu56SfoJ/vNiPqiHZcg75AnBkWNcjhQ= gitlab.com/distributed_lab/running v0.0.0-20200706131153-4af0e83eb96c h1:cpIjV8C//7sLVvMcBaFGopI8sMcCw8Za7T0HIf52esU= gitlab.com/distributed_lab/running v0.0.0-20200706131153-4af0e83eb96c/go.mod h1:4TnADX84dQjQMRHKIMPCVL0L97rD/Jxv0xDbrN6aKzk= -gitlab.com/distributed_lab/urlval/v4 v4.0.3 h1:ZgdSBcvaoHBYmgze/u0bYfvq5Xx47pGTdfFmMYxn27s= -gitlab.com/distributed_lab/urlval/v4 v4.0.3/go.mod h1:IdRM8gOyzpXNoAkIKWVwN+dChh6+1TioS/SVhTGvRFA= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1408,8 +1396,6 @@ 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.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -2061,8 +2047,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/assets/migrations/001_initial.sql b/internal/assets/migrations/001_initial.sql index 6d116dd..c195cf4 100644 --- a/internal/assets/migrations/001_initial.sql +++ b/internal/assets/migrations/001_initial.sql @@ -7,4 +7,4 @@ CREATE TABLE participants ); -- +migrate Down -DROP TABLE participants; +DROP TABLE participants; \ No newline at end of file diff --git a/internal/assets/migrations/002_tx_info.sql b/internal/assets/migrations/002_tx_info.sql new file mode 100644 index 0000000..1f941bd --- /dev/null +++ b/internal/assets/migrations/002_tx_info.sql @@ -0,0 +1,17 @@ +-- +migrate Up +CREATE TYPE tx_status_enum AS ENUM ('pending', 'completed', 'failed'); + +ALTER TABLE participants + ADD COLUMN status tx_status_enum NOT NULL DEFAULT 'completed', + ADD COLUMN updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), + ADD COLUMN tx_hash VARCHAR(64) NOT NULL DEFAULT '', + ADD COLUMN amount TEXT NOT NULL DEFAULT '0urmo'; + +-- +migrate Down +ALTER TABLE participants + DROP COLUMN status, + DROP COLUMN updated_at, + DROP COLUMN tx_hash, + DROP COLUMN amount; + +DROP TYPE IF EXISTS tx_status_enum; diff --git a/internal/broadcaster/broadcaster.go b/internal/broadcaster/broadcaster.go new file mode 100644 index 0000000..d44b3f6 --- /dev/null +++ b/internal/broadcaster/broadcaster.go @@ -0,0 +1,211 @@ +// Package broadcaster provides the functionality to broadcast transactions to +// the blockchain. It is similar to https://github.com/rarimo/broadcaster-svc, +// but is integrated into airdrop-svc purposely. The mentioned broadcaster does +// not allow you to track even successful transaction submission. +// +// The reason of broadcasting implementation is the same: account sequence +// (nonce) must be strictly incrementing in Cosmos. +package broadcaster + +import ( + "context" + "fmt" + "time" + + clienttx "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/types" + client "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/rarimo/airdrop-svc/internal/config" + "github.com/rarimo/airdrop-svc/internal/data" + ethermint "github.com/rarimo/rarimo-core/ethermint/types" + "gitlab.com/distributed_lab/logan/v3" + "gitlab.com/distributed_lab/running" +) + +const txCodeSuccess = 0 + +type Runner struct { + log *logan.Entry + participants *data.ParticipantsQ + config.Broadcaster +} + +func Run(ctx context.Context, cfg *config.Config) { + log := cfg.Log().WithField("service", "builtin-broadcaster") + log.Info("Starting service") + + r := &Runner{ + log: log, + participants: data.NewParticipantsQ(cfg.DB().Clone()), + Broadcaster: cfg.Broadcaster(), + } + + running.WithBackOff(ctx, r.log, "builtin-broadcaster", r.run, 5*time.Second, 5*time.Second, 5*time.Second) +} + +func (r *Runner) run(ctx context.Context) error { + participants, err := r.participants.New().FilterByStatus(data.TxStatusPending).Limit(r.QueryLimit).Select() + if err != nil { + return fmt.Errorf("select participants: %w", err) + } + if len(participants) == 0 { + return nil + } + r.log.Debugf("Got %d participants to broadcast airdrop transactions", len(participants)) + + for _, participant := range participants { + log := r.log.WithField("participant_nullifier", participant.Nullifier) + + if err := r.handleParticipant(ctx, participant); err != nil { + log.WithError(err).Error("Failed to handle participant") + continue + } + } + + return nil +} + +func (r *Runner) handleParticipant(ctx context.Context, participant data.Participant) error { + tx, err := r.createAirdropTx(ctx, participant) + if err != nil { + return fmt.Errorf("creating airdrop tx: %w", err) + } + + txHash, err := r.broadcastTx(ctx, tx) + if err != nil { + err = r.participants.New().UpdateStatus(participant.Nullifier, txHash, data.TxStatusFailed) + if err != nil { + return fmt.Errorf("update participant failed tx status: %w", err) + } + + return fmt.Errorf("broadcast tx: %w", err) + } + + err = r.participants.New().UpdateStatus(participant.Nullifier, txHash, data.TxStatusCompleted) + if err != nil { + return fmt.Errorf("update participant completed tx status: %w", err) + } + + return nil +} + +func (r *Runner) createAirdropTx(ctx context.Context, participant data.Participant) ([]byte, error) { + tx, err := r.genTx(ctx, 0, participant) + if err != nil { + return nil, fmt.Errorf("failed to generate tx: %w", err) + } + + gasUsed, err := r.simulateTx(ctx, tx) + if err != nil { + return nil, fmt.Errorf("failed to simulate tx: %w", err) + } + + tx, err = r.genTx(ctx, gasUsed*3, participant) + if err != nil { + return nil, fmt.Errorf("failed to generate tx after simulation: %w", err) + } + + return tx, nil +} + +func (r *Runner) genTx(ctx context.Context, gasLimit uint64, p data.Participant) ([]byte, error) { + tx, err := r.buildTransferTx(p) + if err != nil { + return nil, fmt.Errorf("build transfer tx: %w", err) + } + + builder, err := r.TxConfig.WrapTxBuilder(tx) + if err != nil { + return nil, fmt.Errorf("wrap tx with builder: %w", err) + } + builder.SetGasLimit(gasLimit) + // there are no fees on the mainnet now, and applies fees requires a lot of work + builder.SetFeeAmount(types.Coins{types.NewInt64Coin("urmo", 0)}) + + resp, err := r.Auth.Account(ctx, &authtypes.QueryAccountRequest{Address: r.SenderAddress}) + if err != nil { + return nil, fmt.Errorf("get sender account: %w", err) + } + + var account ethermint.EthAccount + if err = account.Unmarshal(resp.Account.Value); err != nil { + return nil, fmt.Errorf("unmarshal sender account: %w", err) + } + + err = builder.SetSignatures(signing.SignatureV2{ + PubKey: r.Sender.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: r.TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: account.Sequence, + }) + if err != nil { + return nil, fmt.Errorf("set signatures to tx: %w", err) + } + + signerData := xauthsigning.SignerData{ + ChainID: r.ChainID, + AccountNumber: account.AccountNumber, + Sequence: account.Sequence, + } + sigV2, err := clienttx.SignWithPrivKey( + r.TxConfig.SignModeHandler().DefaultMode(), signerData, + builder, r.Sender, r.TxConfig, account.Sequence, + ) + if err != nil { + return nil, fmt.Errorf("sign with private key: %w", err) + } + + if err = builder.SetSignatures(sigV2); err != nil { + return nil, fmt.Errorf("set signatures V2: %w", err) + } + + return r.TxConfig.TxEncoder()(builder.GetTx()) +} + +func (r *Runner) simulateTx(ctx context.Context, tx []byte) (gasUsed uint64, err error) { + sim, err := r.TxClient.Simulate(ctx, &client.SimulateRequest{TxBytes: tx}) + if err != nil { + return 0, fmt.Errorf("simulate tx: %w", err) + } + + r.log.Debugf("Gas wanted: %d; gas used in simulation: %d", sim.GasInfo.GasWanted, sim.GasInfo.GasUsed) + return sim.GasInfo.GasUsed, nil +} + +func (r *Runner) broadcastTx(ctx context.Context, tx []byte) (string, error) { + grpcRes, err := r.TxClient.BroadcastTx(ctx, &client.BroadcastTxRequest{ + Mode: client.BroadcastMode_BROADCAST_MODE_BLOCK, + TxBytes: tx, + }) + if err != nil { + return "", fmt.Errorf("send tx: %w", err) + } + r.log.Debugf("Submitted transaction to the core: %s", grpcRes.TxResponse.TxHash) + + if grpcRes.TxResponse.Code != txCodeSuccess { + return grpcRes.TxResponse.TxHash, fmt.Errorf("got error code: %d, info: %s, log: %s", grpcRes.TxResponse.Code, grpcRes.TxResponse.Info, grpcRes.TxResponse.RawLog) + } + + return grpcRes.TxResponse.TxHash, nil +} + +func (r *Runner) buildTransferTx(p data.Participant) (types.Tx, error) { + tx := &bank.MsgSend{ + FromAddress: r.SenderAddress, + ToAddress: p.Address, + Amount: r.AirdropCoins, + } + + builder := r.TxConfig.NewTxBuilder() + if err := builder.SetMsgs(tx); err != nil { + return nil, fmt.Errorf("set messages: %w", err) + } + + return builder.GetTx(), nil +} diff --git a/internal/cli/main.go b/internal/cli/main.go index 387e5e4..5bf39cf 100644 --- a/internal/cli/main.go +++ b/internal/cli/main.go @@ -8,6 +8,7 @@ import ( "syscall" "github.com/alecthomas/kingpin" + "github.com/rarimo/airdrop-svc/internal/broadcaster" "github.com/rarimo/airdrop-svc/internal/config" "github.com/rarimo/airdrop-svc/internal/service" "gitlab.com/distributed_lab/kit/kv" @@ -42,7 +43,7 @@ func Run(args []string) bool { defer stop() var wg sync.WaitGroup - run := func(f func(context.Context, config.Config)) { + run := func(f func(context.Context, *config.Config)) { wg.Add(1) go func() { f(ctx, cfg) @@ -53,6 +54,7 @@ func Run(args []string) bool { switch cmd { case serviceCmd.FullCommand(): run(service.Run) + run(broadcaster.Run) case migrateUpCmd.FullCommand(): err = MigrateUp(cfg) case migrateDownCmd.FullCommand(): diff --git a/internal/cli/migrate.go b/internal/cli/migrate.go index e7b9d2b..906a0fc 100644 --- a/internal/cli/migrate.go +++ b/internal/cli/migrate.go @@ -12,7 +12,7 @@ var migrations = &migrate.EmbedFileSystemMigrationSource{ Root: "migrations", } -func MigrateUp(cfg config.Config) error { +func MigrateUp(cfg *config.Config) error { applied, err := migrate.Exec(cfg.DB().RawDB(), "postgres", migrations, migrate.Up) if err != nil { return errors.Wrap(err, "failed to apply migrations") @@ -21,7 +21,7 @@ func MigrateUp(cfg config.Config) error { return nil } -func MigrateDown(cfg config.Config) error { +func MigrateDown(cfg *config.Config) error { applied, err := migrate.Exec(cfg.DB().RawDB(), "postgres", migrations, migrate.Down) if err != nil { return errors.Wrap(err, "failed to apply migrations") diff --git a/internal/config/broadcaster.go b/internal/config/broadcaster.go new file mode 100644 index 0000000..37f8cb9 --- /dev/null +++ b/internal/config/broadcaster.go @@ -0,0 +1,112 @@ +package config + +import ( + "fmt" + "time" + + sdkclient "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" + txclient "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/common/hexutil" + "gitlab.com/distributed_lab/figure/v3" + "gitlab.com/distributed_lab/kit/comfig" + "gitlab.com/distributed_lab/kit/kv" + "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" +) + +const accountPrefix = "rarimo" + +type Broadcaster struct { + AirdropCoins types.Coins + Sender cryptotypes.PrivKey + SenderAddress string + ChainID string + TxConfig sdkclient.TxConfig + TxClient txclient.ServiceClient + Auth authtypes.QueryClient + QueryLimit uint64 +} + +type Broadcasterer interface { + Broadcaster() Broadcaster +} + +type broadcasterer struct { + getter kv.Getter + once comfig.Once +} + +func NewBroadcaster(getter kv.Getter) Broadcasterer { + return &broadcasterer{ + getter: getter, + } +} + +func (b *broadcasterer) Broadcaster() Broadcaster { + return b.once.Do(func() interface{} { + var cfg struct { + AirdropAmount string `fig:"airdrop_amount,required"` + CosmosRPC string `fig:"cosmos_rpc,required"` + ChainID string `fig:"chain_id,required"` + SenderPrivateKey string `fig:"sender_private_key,required"` + QueryLimit uint64 `fig:"query_limit"` + } + + err := figure.Out(&cfg).From(kv.MustGetStringMap(b.getter, "broadcaster")).Please() + if err != nil { + panic(fmt.Errorf("failed to figure out broadcaster: %w", err)) + } + + amount, err := types.ParseCoinsNormalized(cfg.AirdropAmount) + if err != nil { + panic(fmt.Errorf("broadcaster: invalid airdrop amount: %w", err)) + } + + cosmosRPC, err := grpc.Dial(cfg.CosmosRPC, grpc.WithInsecure(), grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: 10 * time.Second, // wait time before ping if no activity + Timeout: 20 * time.Second, // ping timeout + })) + if err != nil { + panic(fmt.Errorf("broadcaster: failed to dial cosmos core rpc: %w", err)) + } + + privateKey, err := hexutil.Decode(cfg.SenderPrivateKey) + if err != nil { + panic(fmt.Errorf("broadcaster: sender private key is not a hex string: %w", err)) + } + + sender := &secp256k1.PrivKey{Key: privateKey} + address, err := bech32.ConvertAndEncode(accountPrefix, sender.PubKey().Address().Bytes()) + if err != nil { + panic(fmt.Errorf("failed to convert and encode sender address: %w", err)) + } + + queryLimit := uint64(100) + if cfg.QueryLimit > 0 { + queryLimit = cfg.QueryLimit + } + + return Broadcaster{ + Sender: sender, + SenderAddress: address, + ChainID: cfg.ChainID, + TxConfig: authtx.NewTxConfig( + codec.NewProtoCodec(codectypes.NewInterfaceRegistry()), + []signing.SignMode{signing.SignMode_SIGN_MODE_DIRECT}, + ), + TxClient: txclient.NewServiceClient(cosmosRPC), + Auth: authtypes.NewQueryClient(cosmosRPC), + AirdropCoins: amount, + QueryLimit: queryLimit, + } + }).(Broadcaster) +} diff --git a/internal/config/main.go b/internal/config/main.go index 862925e..66a7913 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -1,7 +1,6 @@ package config import ( - "github.com/rarimo/saver-grpc-lib/broadcaster" "gitlab.com/distributed_lab/kit/comfig" "gitlab.com/distributed_lab/kit/kv" "gitlab.com/distributed_lab/kit/pgdb" @@ -11,18 +10,19 @@ type Config struct { comfig.Logger pgdb.Databaser comfig.Listenerer - broadcaster.Broadcasterer + Broadcasterer - airdrop comfig.Once - getter kv.Getter + airdrop comfig.Once + verifier comfig.Once + getter kv.Getter } -func New(getter kv.Getter) Config { - return Config{ +func New(getter kv.Getter) *Config { + return &Config{ getter: getter, Databaser: pgdb.NewDatabaser(getter), Listenerer: comfig.NewListenerer(getter), Logger: comfig.NewLogger(getter, comfig.LoggerOpts{}), - Broadcasterer: broadcaster.New(getter), + Broadcasterer: NewBroadcaster(getter), } } diff --git a/internal/config/verifier.go b/internal/config/verifier.go new file mode 100644 index 0000000..1f6b573 --- /dev/null +++ b/internal/config/verifier.go @@ -0,0 +1,54 @@ +package config + +import ( + "fmt" + "os" + + "gitlab.com/distributed_lab/figure/v3" + "gitlab.com/distributed_lab/kit/kv" +) + +type VerifierConfig struct { + VerificationKeys map[string][]byte + AllowedAge int + AllowedCitizenships []interface{} // more convenient to use for validation, replace on need +} + +func (c *Config) Verifier() *VerifierConfig { + return c.verifier.Do(func() interface{} { + var cfg struct { + VerificationKeysPaths map[string]string `fig:"verification_keys_paths,required"` + AllowedAge int `fig:"allowed_age,required"` + AllowedCitizenships []string `fig:"allowed_citizenships,required"` + } + + err := figure. + Out(&cfg). + With(figure.BaseHooks). + From(kv.MustGetStringMap(c.getter, "verifier")). + Please() + if err != nil { + panic(fmt.Errorf("failed to figure out verifier: %w", err)) + } + + verificationKeys := make(map[string][]byte) + for algo, path := range cfg.VerificationKeysPaths { + verificationKey, err := os.ReadFile(path) + if err != nil { + panic(fmt.Errorf("failed to read verification key file: %w", err)) + } + verificationKeys[algo] = verificationKey + } + + citizenships := make([]interface{}, len(cfg.AllowedCitizenships)) + for i, ctz := range cfg.AllowedCitizenships { + citizenships[i] = ctz + } + + return &VerifierConfig{ + VerificationKeys: verificationKeys, + AllowedAge: cfg.AllowedAge, + AllowedCitizenships: citizenships, + } + }).(*VerifierConfig) +} diff --git a/internal/data/participants.go b/internal/data/participants.go index b219e6d..fac2790 100644 --- a/internal/data/participants.go +++ b/internal/data/participants.go @@ -1,4 +1,4 @@ -package pg +package data import ( "database/sql" @@ -10,12 +10,22 @@ import ( "gitlab.com/distributed_lab/kit/pgdb" ) +const ( + TxStatusPending = "pending" + TxStatusCompleted = "completed" + TxStatusFailed = "failed" +) + const participantsTable = "participants" type Participant struct { Nullifier string `db:"nullifier"` Address string `db:"address"` + Status string `db:"status"` + TxHash string `db:"tx_hash"` + Amount string `db:"amount"` CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type ParticipantsQ struct { @@ -34,11 +44,41 @@ func (q *ParticipantsQ) New() *ParticipantsQ { return NewParticipantsQ(q.db) } -func (q *ParticipantsQ) Insert(nullifier, address string) error { - stmt := squirrel.Insert(participantsTable).Columns("nullifier").Values(nullifier) +func (q *ParticipantsQ) Insert(p Participant) (*Participant, error) { + var res Participant + stmt := squirrel.Insert(participantsTable).SetMap(map[string]interface{}{ + "nullifier": p.Nullifier, + "address": p.Address, + "status": p.Status, + "tx_hash": p.TxHash, + "amount": p.Amount, + }).Suffix("RETURNING *") + + if err := q.db.Get(&res, stmt); err != nil { + return nil, fmt.Errorf("insert participant %+v: %w", p, err) + } + + return &res, nil +} + +func (q *ParticipantsQ) UpdateStatus(nullifier, txHash, status string) error { + stmt := squirrel.Update(participantsTable). + Set("status", status). + Set("tx_hash", txHash). + Where(squirrel.Eq{"nullifier": nullifier}) if err := q.db.Exec(stmt); err != nil { - return fmt.Errorf("insert participant %s: %w", nullifier, err) + return fmt.Errorf("update participant status [nullifier=%s newStatus=%s]: %w", nullifier, status, err) + } + + return nil +} + +func (q *ParticipantsQ) Delete(nullifier string) error { + stmt := squirrel.Delete(participantsTable).Where(squirrel.Eq{"nullifier": nullifier}) + + if err := q.db.Exec(stmt); err != nil { + return fmt.Errorf("delete participant [nullifier=%s]: %w", nullifier, err) } return nil @@ -71,3 +111,13 @@ func (q *ParticipantsQ) Get(nullifier string) (*Participant, error) { return &res, nil } + +func (q *ParticipantsQ) Limit(limit uint64) *ParticipantsQ { + q.selector = q.selector.Limit(limit) + return q +} + +func (q *ParticipantsQ) FilterByStatus(status string) *ParticipantsQ { + q.selector = q.selector.Where(squirrel.Eq{"status": status}) + return q +} diff --git a/internal/service/handlers/create_airdrop.go b/internal/service/handlers/create_airdrop.go index d1944e7..61acff8 100644 --- a/internal/service/handlers/create_airdrop.go +++ b/internal/service/handlers/create_airdrop.go @@ -3,23 +3,37 @@ package handlers import ( "fmt" "net/http" + "strings" - cosmos "github.com/cosmos/cosmos-sdk/types" - bank "github.com/cosmos/cosmos-sdk/x/bank/types" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/iden3/go-rapidsnark/verifier" + "github.com/rarimo/airdrop-svc/internal/config" + "github.com/rarimo/airdrop-svc/internal/data" "github.com/rarimo/airdrop-svc/internal/service/requests" "github.com/rarimo/airdrop-svc/resources" "gitlab.com/distributed_lab/ape" "gitlab.com/distributed_lab/ape/problems" ) +// Full list of the OpenSSL signature algorithms and hash-functions is provided here: +// https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set1_sigalgs_list.html + +const ( + sha256rsa = "SHA256withRSA" + sha1ecdsa = "SHA1withECDSA" + sha256ecdsa = "SHA256withECDSA" +) + func CreateAirdrop(w http.ResponseWriter, r *http.Request) { - req, err := requests.NewCreateAirdrop(r) + req, err := requests.NewCreateAirdrop(r, Verifier(r)) if err != nil { ape.RenderErr(w, problems.BadRequest(err)...) return } - participant, err := ParticipantsQ(r).Get(req.Data.ID) + nullifier := req.Data.Attributes.ZkProof.PubSignals[requests.PubSignalNullifier] + + participant, err := ParticipantsQ(r).Get(nullifier) if err != nil { Log(r).WithError(err).Error("Failed to get participant by ID") ape.RenderErr(w, problems.InternalError()) @@ -30,35 +44,79 @@ func CreateAirdrop(w http.ResponseWriter, r *http.Request) { return } - err = ParticipantsQ(r).Transaction(func() error { - err = ParticipantsQ(r).Insert(req.Data.ID, req.Data.Attributes.Address) - if err != nil { - return fmt.Errorf("insert participant: %w", err) - } - return broadcastWithdrawalTx(req, r) - }) + if err = verifyProof(req, Verifier(r)); err != nil { + Log(r).WithError(err).Info("Invalid proof") + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "data/attributes/zk_proof": err, + })...) + return + } + participant, err = ParticipantsQ(r).Insert(data.Participant{ + Nullifier: nullifier, + Address: req.Data.Attributes.Address, + Status: data.TxStatusPending, + Amount: AirdropAmount(r), + }) if err != nil { - Log(r).WithError(err).Error("Failed to save and perform airdrop") + Log(r).WithError(err).WithField("nullifier", nullifier).Errorf("Failed to insert participant") ape.RenderErr(w, problems.InternalError()) return } - w.WriteHeader(http.StatusNoContent) + ape.Render(w, toAirdropResponse(*participant)) } -func broadcastWithdrawalTx(req resources.CreateAirdropRequest, r *http.Request) error { - urmo := AirdropAmount(r) - tx := &bank.MsgSend{ - FromAddress: Broadcaster(r).Sender(), - ToAddress: req.Data.Attributes.Address, - Amount: cosmos.NewCoins(cosmos.NewInt64Coin("urmo", urmo)), +func verifyProof(req resources.CreateAirdropRequest, cfg *config.VerifierConfig) error { + var key []byte + algorithm := signatureAlgorithm(req.Data.Attributes.Algorithm) + switch algorithm { + case sha1ecdsa: + key = cfg.VerificationKeys["sha1"] + case sha256rsa, sha256ecdsa: + key = cfg.VerificationKeys["sha256"] + default: + return fmt.Errorf("unsupported algorithm: %s", req.Data.Attributes.Algorithm) } - err := Broadcaster(r).BroadcastTx(r.Context(), tx) - if err != nil { - return fmt.Errorf("broadcast withdrawal tx: %w", err) + proof := req.Data.Attributes.ZkProof + if err := verifier.VerifyGroth16(proof, key); err != nil { + return fmt.Errorf("verify groth16: %w", err) } return nil } + +var algorithmsMap = map[string]map[string]string{ + "SHA1": { + "ECDSA": sha1ecdsa, + }, + "SHA256": { + "RSA": sha256rsa, + "ECDSA": sha256ecdsa, + }, +} + +func signatureAlgorithm(passedAlgorithm string) string { + if passedAlgorithm == "rsaEncryption" { + return sha256rsa + } + + if strings.Contains(strings.ToUpper(passedAlgorithm), "PSS") { + return "" // RSA-PSS is not currently supported + } + + for hashFunc, signatureAlgorithms := range algorithmsMap { + if !strings.Contains(strings.ToUpper(passedAlgorithm), hashFunc) { + continue + } + + for signatureAlgo, algorithmName := range signatureAlgorithms { + if strings.Contains(strings.ToUpper(passedAlgorithm), signatureAlgo) { + return algorithmName + } + } + } + + return "" +} diff --git a/internal/service/handlers/ctx.go b/internal/service/handlers/ctx.go index 15491e8..5cbbe19 100644 --- a/internal/service/handlers/ctx.go +++ b/internal/service/handlers/ctx.go @@ -4,8 +4,8 @@ import ( "context" "net/http" - data "github.com/rarimo/airdrop-svc/internal/data" - "github.com/rarimo/saver-grpc-lib/broadcaster" + "github.com/rarimo/airdrop-svc/internal/config" + "github.com/rarimo/airdrop-svc/internal/data" "gitlab.com/distributed_lab/logan/v3" ) @@ -15,7 +15,7 @@ const ( logCtxKey ctxKey = iota participantsQCtxKey airdropAmountCtxKey - broadcasterCtxKey + verifierCtxKey ) func CtxLog(entry *logan.Entry) func(context.Context) context.Context { @@ -38,22 +38,22 @@ func ParticipantsQ(r *http.Request) *data.ParticipantsQ { return r.Context().Value(participantsQCtxKey).(*data.ParticipantsQ).New() } -func CtxAirdropAmount(amount int64) func(context.Context) context.Context { +func CtxAirdropAmount(amount string) func(context.Context) context.Context { return func(ctx context.Context) context.Context { return context.WithValue(ctx, airdropAmountCtxKey, amount) } } -func AirdropAmount(r *http.Request) int64 { - return r.Context().Value(airdropAmountCtxKey).(int64) +func AirdropAmount(r *http.Request) string { + return r.Context().Value(airdropAmountCtxKey).(string) } -func CtxBroadcaster(broadcaster broadcaster.Broadcaster) func(context.Context) context.Context { +func CtxVerifier(entry *config.VerifierConfig) func(context.Context) context.Context { return func(ctx context.Context) context.Context { - return context.WithValue(ctx, broadcasterCtxKey, broadcaster) + return context.WithValue(ctx, verifierCtxKey, entry) } } -func Broadcaster(r *http.Request) broadcaster.Broadcaster { - return r.Context().Value(broadcasterCtxKey).(broadcaster.Broadcaster) +func Verifier(r *http.Request) *config.VerifierConfig { + return r.Context().Value(verifierCtxKey).(*config.VerifierConfig) } diff --git a/internal/service/handlers/get_airdrop.go b/internal/service/handlers/get_airdrop.go new file mode 100644 index 0000000..3049d46 --- /dev/null +++ b/internal/service/handlers/get_airdrop.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "net/http" + + "github.com/go-chi/chi" + validation "github.com/go-ozzo/ozzo-validation/v4" + data "github.com/rarimo/airdrop-svc/internal/data" + "github.com/rarimo/airdrop-svc/resources" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func GetAirdrop(w http.ResponseWriter, r *http.Request) { + var ( + id = chi.URLParam(r, "id") + err = validation.Errors{"{id}": validation.Validate(id, validation.Required)}.Filter() + ) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + participant, err := ParticipantsQ(r).Get(id) + if err != nil { + Log(r).WithError(err).Error("Failed to get participant by ID") + ape.RenderErr(w, problems.InternalError()) + return + } + if participant == nil { + ape.RenderErr(w, problems.NotFound()) + return + } + + ape.Render(w, toAirdropResponse(*participant)) +} + +func toAirdropResponse(p data.Participant) resources.AirdropResponse { + return resources.AirdropResponse{ + Data: resources.Airdrop{ + Key: resources.Key{ + ID: p.Nullifier, + Type: resources.AIRDROP, + }, + Attributes: resources.AirdropAttributes{ + Address: p.Address, + Status: p.Status, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + Amount: p.Amount, + TxHash: p.TxHash, + }, + }, + } +} diff --git a/internal/service/handlers/middleware.go b/internal/service/handlers/middleware.go index 3325fd2..322091e 100644 --- a/internal/service/handlers/middleware.go +++ b/internal/service/handlers/middleware.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - data "github.com/rarimo/airdrop-svc/internal/data" + "github.com/rarimo/airdrop-svc/internal/data" "gitlab.com/distributed_lab/kit/pgdb" ) diff --git a/internal/service/requests/create_airdrop.go b/internal/service/requests/create_airdrop.go index f83c9a6..fc224e7 100644 --- a/internal/service/requests/create_airdrop.go +++ b/internal/service/requests/create_airdrop.go @@ -3,27 +3,82 @@ package requests import ( "encoding/json" "fmt" + "math/big" "net/http" + "time" - validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/decred/dcrd/bech32" + val "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/airdrop-svc/internal/config" "github.com/rarimo/airdrop-svc/resources" ) -func NewCreateAirdrop(r *http.Request) (req resources.CreateAirdropRequest, err error) { +const ( + PubSignalNullifier = iota + pubSignalBirthDate + pubSignalExpirationDate + pubSignalCitizenship = 6 + pubSignalEventID = 9 + pubSignalEventData = 10 + pubSignalSelector = 12 + + proofSelectorValue = "39" + proofEventIDValue = "ac42d1a986804618c7a793fbe814d9b31e47be51e082806363dca6958f3062" +) + +func NewCreateAirdrop(r *http.Request, cfg *config.VerifierConfig) (req resources.CreateAirdropRequest, err error) { if err = json.NewDecoder(r.Body).Decode(&req); err != nil { - err = newDecodeError("body", err) - return + return req, newDecodeError("body", err) + } + + var ( + attr = req.Data.Attributes + signals = attr.ZkProof.PubSignals + olderThanDate = time.Now().UTC().AddDate(-cfg.AllowedAge, 0, 0) + ) + + err = val.Errors{ + "data/type": val.Validate(req.Data.Type, val.Required, val.In(resources.CREATE_AIRDROP)), + "data/attributes/address": val.Validate(attr.Address, val.Required, isRarimoAddr), + "data/attributes/algorithm": val.Validate(attr.Algorithm, val.Required), + "data/attributes/zk_proof/proof": val.Validate(attr.ZkProof.Proof, val.Required), + "data/attributes/zk_proof/pub_signals": val.Validate(signals, val.Required, val.Length(14, 14)), + }.Filter() + if err != nil { + return req, err + } + + eventID, ok := new(big.Int).SetString(signals[pubSignalEventID], 10) + if !ok { + return req, newDecodeError( + "pub_signals/event_id", + fmt.Errorf("setting string %s", signals[pubSignalEventID]), + ) } - return req, validation.Errors{ - "data/id": validation.Validate(req.Data.ID, validation.Required), - "data/type": validation.Validate(req.Data.Type, validation.Required, validation.In(resources.CREATE_AIRDROP)), - "data/attributes/address": validation.Validate(req.Data.Attributes.Address, validation.Required), + _, addrBytes, err := bech32.Decode(attr.Address) + if err != nil { + return req, newDecodeError("data/attributes/address", err) + } + + var ( + addrDec = encodeInt(addrBytes) + citizenship = decodeInt(signals[pubSignalCitizenship]) + ) + + return req, val.Errors{ + "pub_signals/nullifier": val.Validate(signals[PubSignalNullifier], val.Required), + "pub_signals/selector": val.Validate(signals[pubSignalSelector], val.Required, val.In(proofSelectorValue)), + "pub_signals/expiration_date": val.Validate(signals[pubSignalExpirationDate], val.Required, afterDate(time.Now().UTC())), + "pub_signals/birth_date": val.Validate(signals[pubSignalBirthDate], val.Required, beforeDate(olderThanDate)), + "pub_signals/citizenship": val.Validate(citizenship, val.Required, val.In(cfg.AllowedCitizenships...)), + "pub_signals/event_id": val.Validate(eventID.Text(16), val.Required, val.In(proofEventIDValue)), + "pub_signals/event_data": val.Validate(signals[pubSignalEventData], val.Required, val.In(addrDec)), }.Filter() } func newDecodeError(what string, err error) error { - return validation.Errors{ + return val.Errors{ what: fmt.Errorf("decode request %s: %w", what, err), } } diff --git a/internal/service/requests/validation.go b/internal/service/requests/validation.go new file mode 100644 index 0000000..592aacb --- /dev/null +++ b/internal/service/requests/validation.go @@ -0,0 +1,79 @@ +package requests + +import ( + "errors" + "fmt" + "math/big" + "time" + + "github.com/cosmos/cosmos-sdk/types" +) + +var isRarimoAddr addressRule + +type ( + addressRule struct{} + timeRule struct { + point time.Time + isBefore bool + } +) + +func (r addressRule) Validate(data interface{}) error { + str, ok := data.(string) + if !ok { + return fmt.Errorf("invalid type: %T, expected string", data) + } + + _, err := types.AccAddressFromBech32(str) + if err != nil { + return fmt.Errorf("invalid bech32 address: %w", err) + } + + return nil +} + +func (r timeRule) Validate(date interface{}) error { + raw, ok := date.(string) + if !ok { + return fmt.Errorf("invalid type: %T, expected string", date) + } + + parsed, err := time.Parse("060102", decodeInt(raw)) + if err != nil { + return fmt.Errorf("invalid date string: %w", err) + } + + if r.isBefore && parsed.After(r.point) { + return errors.New("date is too late") + } + + if !r.isBefore && parsed.Before(r.point) { + return errors.New("date is too early") + } + + return nil +} + +func beforeDate(point time.Time) timeRule { + return timeRule{ + point: point, + isBefore: true, + } +} + +func afterDate(point time.Time) timeRule { + return timeRule{ + point: point, + isBefore: false, + } +} + +func encodeInt(b []byte) string { + return new(big.Int).SetBytes(b).String() +} + +func decodeInt(s string) string { + b, _ := new(big.Int).SetString(s, 10) + return string(b.Bytes()) +} diff --git a/internal/service/router.go b/internal/service/router.go index 6e440bd..4b6d443 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -3,13 +3,15 @@ package service import ( "context" + "github.com/cosmos/cosmos-sdk/types" "github.com/go-chi/chi" "github.com/rarimo/airdrop-svc/internal/config" "github.com/rarimo/airdrop-svc/internal/service/handlers" "gitlab.com/distributed_lab/ape" ) -func Run(ctx context.Context, cfg config.Config) { +func Run(ctx context.Context, cfg *config.Config) { + setBech32Prefixes() r := chi.NewRouter() r.Use( @@ -17,15 +19,24 @@ func Run(ctx context.Context, cfg config.Config) { ape.LoganMiddleware(cfg.Log()), ape.CtxMiddleware( handlers.CtxLog(cfg.Log()), - handlers.CtxAirdropAmount(cfg.AirdropAmount()), - handlers.CtxBroadcaster(cfg.Broadcaster()), + handlers.CtxVerifier(cfg.Verifier()), + handlers.CtxAirdropAmount(cfg.Broadcaster().AirdropCoins.String()), ), handlers.DBCloneMiddleware(cfg.DB()), ) - r.Route("/integrations/airdrop-svc", func(r chi.Router) { - r.Post("/airdrops", handlers.CreateAirdrop) + r.Route("/integrations/airdrop-svc/airdrops", func(r chi.Router) { + r.Post("/", handlers.CreateAirdrop) + r.Get("/{id}", handlers.GetAirdrop) }) cfg.Log().Info("Service started") ape.Serve(ctx, r, cfg, ape.ServeOpts{}) } + +func setBech32Prefixes() { + c := types.GetConfig() + c.SetBech32PrefixForAccount("rarimo", "rarimopub") + c.SetBech32PrefixForValidator("rarimovaloper", "rarimovaloperpub") + c.SetBech32PrefixForConsensusNode("rarimovalcons", "rarimovalconspub") + c.Seal() +} diff --git a/resources/model_airdrop.go b/resources/model_airdrop.go new file mode 100644 index 0000000..5fd7d5f --- /dev/null +++ b/resources/model_airdrop.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type Airdrop struct { + Key + Attributes AirdropAttributes `json:"attributes"` +} +type AirdropResponse struct { + Data Airdrop `json:"data"` + Included Included `json:"included"` +} + +type AirdropListResponse struct { + Data []Airdrop `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *AirdropListResponse) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *AirdropListResponse) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustAirdrop - returns Airdrop from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustAirdrop(key Key) *Airdrop { + var airdrop Airdrop + if c.tryFindEntry(key, &airdrop) { + return &airdrop + } + return nil +} diff --git a/resources/model_airdrop_attributes.go b/resources/model_airdrop_attributes.go new file mode 100644 index 0000000..04d00c3 --- /dev/null +++ b/resources/model_airdrop_attributes.go @@ -0,0 +1,22 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "time" + +type AirdropAttributes struct { + // Destination address for the airdrop + Address string `json:"address"` + // Amount of airdropped coins + Amount string `json:"amount"` + // RFC3339 UTC timestamp of the airdrop creation + CreatedAt time.Time `json:"created_at"` + // Status of the airdrop transaction + Status string `json:"status"` + // Hash of the airdrop transaction + TxHash string `json:"tx_hash"` + // RFC3339 UTC timestamp of the airdrop successful tx + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/resources/model_create_airdrop_attributes.go b/resources/model_create_airdrop_attributes.go index 3364483..6dfb49e 100644 --- a/resources/model_create_airdrop_attributes.go +++ b/resources/model_create_airdrop_attributes.go @@ -4,9 +4,13 @@ package resources +import "github.com/iden3/go-rapidsnark/types" + type CreateAirdropAttributes struct { // Destination address for the airdrop Address string `json:"address"` - // Placeholder for the proof - Proof string `json:"proof"` + // Signing algorithm used in proof. The value from passport document SOD is assumed. + Algorithm string `json:"algorithm"` + // ZK-proof of the passport data + ZkProof types.ZKProof `json:"zk_proof"` } diff --git a/resources/model_resource_type.go b/resources/model_resource_type.go index 69376c0..badfc92 100644 --- a/resources/model_resource_type.go +++ b/resources/model_resource_type.go @@ -8,5 +8,6 @@ type ResourceType string // List of ResourceType const ( + AIRDROP ResourceType = "airdrop" CREATE_AIRDROP ResourceType = "create_airdrop" ) diff --git a/verification_key.json b/verification_key.json new file mode 100644 index 0000000..e5feb38 --- /dev/null +++ b/verification_key.json @@ -0,0 +1,159 @@ +{ + "protocol": "groth16", + "curve": "bn128", + "nPublic": 14, + "vk_alpha_1": [ + "20491192805390485299153009773594534940189261866228447918068658471970481763042", + "9383485363053290200918347156157836566562967994039712273449902621266178545958", + "1" + ], + "vk_beta_2": [ + [ + "6375614351688725206403948262868962793625744043794305715222011528459656738731", + "4252822878758300859123897981450591353533073413197771768651442665752259397132" + ], + [ + "10505242626370262277552901082094356697409835680220590971873171140371331206856", + "21847035105528745403288232691147584728191162732299865338377159692350059136679" + ], + [ + "1", + "0" + ] + ], + "vk_gamma_2": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + [ + "1", + "0" + ] + ], + "vk_delta_2": [ + [ + "18902490647006052583680684949147035157755711876364177649770849967939344884538", + "19432663951169219714199180832722185001550181884984488837398124950285484557723" + ], + [ + "14591094483196085995359195833411906307448492969324321979804892684379441339558", + "5999628724148110316059290465418237180160096173074925497597305915922821774275" + ], + [ + "1", + "0" + ] + ], + "vk_alphabeta_12": [ + [ + [ + "2029413683389138792403550203267699914886160938906632433982220835551125967885", + "21072700047562757817161031222997517981543347628379360635925549008442030252106" + ], + [ + "5940354580057074848093997050200682056184807770593307860589430076672439820312", + "12156638873931618554171829126792193045421052652279363021382169897324752428276" + ], + [ + "7898200236362823042373859371574133993780991612861777490112507062703164551277", + "7074218545237549455313236346927434013100842096812539264420499035217050630853" + ] + ], + [ + [ + "7077479683546002997211712695946002074877511277312570035766170199895071832130", + "10093483419865920389913245021038182291233451549023025229112148274109565435465" + ], + [ + "4595479056700221319381530156280926371456704509942304414423590385166031118820", + "19831328484489333784475432780421641293929726139240675179672856274388269393268" + ], + [ + "11934129596455521040620786944827826205713621633706285934057045369193958244500", + "8037395052364110730298837004334506829870972346962140206007064471173334027475" + ] + ] + ], + "IC": [ + [ + "21327673885005299961818505230691163479042042144639778148003692955425200736186", + "20233959945660576453804329261660391125548013335600690858049840634649293268283", + "1" + ], + [ + "5903376938520018326778583809880270521491177172275283176042261261236098034824", + "15805384632440663692369401736980130251786386583587437616948123682857972322532", + "1" + ], + [ + "9326220291848943201059535996242761688950495688802773201752943264311010614902", + "2003297437436365064109892185222923126758724737940241242622558954340106805287", + "1" + ], + [ + "5366098896536007408381906435441844593345991512005050024745852263243878409037", + "20740059460552597602943382207610363544017420726626979303837290789685920991121", + "1" + ], + [ + "20404569792849015449375865709121251498645251974471736641306215763339626800609", + "14489212707900410939196283629471436416376431939568850129009863111996688419491", + "1" + ], + [ + "20251029915052268400121048730174983570679491326509635444509076172427695565733", + "8121503923205860425007604133667740884774435904318668933445724290643798160756", + "1" + ], + [ + "5660476746545600816022831155597033244613491613266191153995188957757724232729", + "20927590518031292652210500445169357147761075380271057265732882151587454669838", + "1" + ], + [ + "15225248180906883935064509815159151438282815605390395070094474255232393078622", + "4555050821274533518001959087157774898981791805098919625857641300660114716183", + "1" + ], + [ + "14715365108364930387879568300526707316984035933059473476586781788323469412350", + "12192870055063999123137717117305345559566777164496051945164400177781475641733", + "1" + ], + [ + "2082848515453217463677495129326473847336490112872029650204359132614367908122", + "2173997466818478897370776479935485571774315439446720778925042738415651328109", + "1" + ], + [ + "6646023268191686713660275902219940056013913801689619995927418974603183614887", + "1231806158510607488813861836702722689673636443726119736095369434042057333372", + "1" + ], + [ + "4377359631235333289892095991643823095145640220475443425003268667238699140586", + "9374374232144989733270988720515633221305302580281530317206103302242955523396", + "1" + ], + [ + "8110399313240184355618312860633022154554813418871633597599517326739014864140", + "11676550680226129473408430325024673042473466539753500775196687152830088242971", + "1" + ], + [ + "7024062754746500814827176673987775644012152972941215368751859266417069107618", + "2787628166343310828586499689603219957481834488889340575677676565749708186563", + "1" + ], + [ + "6887254711132875200665861464020284080991043892867693682461427028944957149365", + "1644391122832138403227281593066810887181390293587245530100267365785376294120", + "1" + ] + ] +} \ No newline at end of file diff --git a/werf.yaml b/werf.yaml index b4bcb4a..93ea40c 100644 --- a/werf.yaml +++ b/werf.yaml @@ -42,4 +42,8 @@ import: - image: builder add: /usr/local/bin/airdrop-svc to: /usr/local/bin/airdrop-svc - after: setup \ No newline at end of file + after: setup + - image: builder + add: /go/src/github.com/rarimo/airdrop-svc/verification_key.json + to: /verification_key.json + after: setup