diff --git a/libs/Dockerfile b/libs/Dockerfile index 59006e8b21c..60c7ae7fcac 100644 --- a/libs/Dockerfile +++ b/libs/Dockerfile @@ -3,6 +3,9 @@ RUN apt-get install make WORKDIR /app COPY package*.json ./ +COPY patches/* ./patches/ +# Required to run package-patch: https://github.com/ds300/patch-package/issues/185 +RUN npm config set unsafe-perm true RUN npm install --loglevel verbose FROM node:14.16-alpine diff --git a/libs/package-lock.json b/libs/package-lock.json index c5a9c524b13..32e270b396a 100644 --- a/libs/package-lock.json +++ b/libs/package-lock.json @@ -2132,6 +2132,11 @@ } } }, + "@noble/secp256k1": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.0.tgz", + "integrity": "sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2614,6 +2619,11 @@ } } }, + "@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -4585,6 +4595,12 @@ "@walletconnect/window-getters": "^1.0.0" } }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -5547,6 +5563,12 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, "cids": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/cids/-/cids-0.7.5.tgz", @@ -6078,6 +6100,11 @@ "postcss-value-parser": "^4.0.2" } }, + "cuid": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/cuid/-/cuid-2.1.8.tgz", + "integrity": "sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==" + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -7942,6 +7969,15 @@ "path-exists": "^4.0.0" } }, + "find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "requires": { + "micromatch": "^4.0.2" + } + }, "fix-hmr": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/fix-hmr/-/fix-hmr-1.0.2.tgz", @@ -8710,6 +8746,15 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, "is-core-module": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", @@ -8727,6 +8772,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8899,6 +8950,15 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -9535,6 +9595,15 @@ "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==" }, + "klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11" + } + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -9815,6 +9884,11 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "micro-aes-gcm": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/micro-aes-gcm/-/micro-aes-gcm-0.3.3.tgz", + "integrity": "sha512-wetPK288r0FyTzrWKkX9fNAKlJE+//bdQWpucWwx1Ei/MyFgBgN/0Ex1G3sTm4WR+2ATReGKzDQBYVpuwhho/A==" + }, "micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", @@ -10277,6 +10351,12 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "nise": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", @@ -10576,6 +10656,16 @@ "mimic-fn": "^2.1.0" } }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -10803,6 +10893,169 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "patch-package": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.5.0.tgz", + "integrity": "sha512-tC3EqJmo74yKqfsMzELaFwxOAu6FH6t+FzFOsnWAuARm7/n2xB5AOeOueE221eM9gtMuIKMKpF9tBy/X2mNP0Q==", + "dev": true, + "requires": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "cross-spawn": "^6.0.5", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^7.0.1", + "is-ci": "^2.0.0", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^5.6.0", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^1.10.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -14352,6 +14605,12 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, "yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", diff --git a/libs/package.json b/libs/package.json index 12234a8ac51..3dbfc5cb643 100644 --- a/libs/package.json +++ b/libs/package.json @@ -12,6 +12,7 @@ "./dist/index.cjs.js": "./dist/index.browser.cjs.js", "./dist/index.esm.js": "./dist/index.browser.esm.js" }, + "react-native": "./dist/index.native.js", "scripts": { "init-local": "ts-node initScripts/local.js", "test": "./scripts/test.sh", @@ -20,8 +21,18 @@ "test:unit:watch": "ts-mocha 'src/**/*.test.{js,ts}' --watch", "test:integration": "ts-mocha tests/index.js", "setup": "./scripts/migrate_contracts.sh", - "build": "rollup -c", - "dev": "rollup -c -w", + "build": "rollup -c --configPlugin typescript", + "build:sdk": "rollup -c rollup.sdk.config.ts --configPlugin typescript", + "build:sdk:browser": "rollup -c rollup.sdk.browser.config.ts --configPlugin typescript", + "build:sdk:native": "rollup -c rollup.sdk.native.config.ts --configPlugin typescript", + "build:legacy:browser": "rollup -c rollup.legacy.browser.config.ts --configPlugin typescript", + "build:legacy:native": "rollup -c rollup.legacy.native.config.ts --configPlugin typescript", + "start": "rollup -c -w --configPlugin typescript", + "start:sdk": "rollup -c rollup.sdk.config.ts -w --configPlugin typescript", + "start:sdk:browser": "rollup -c rollup.sdk.browser.config.ts -w --configPlugin typescript", + "start:sdk:native": "rollup -c rollup.sdk.native.config.ts -w --configPlugin typescript", + "start:legacy:browser": "rollup -c rollup.legacy.browser.config.ts -w --configPlugin typescript", + "start:legacy:native": "rollup -c rollup.legacy.native.config.ts -w --configPlugin typescript", "lint": "eslint ./src ./types", "lint:fix": "npm run lint -- --fix", "typecheck": "tsc --noEmit", @@ -34,7 +45,8 @@ "gen:stage:full": "node ./src/sdk/api/generator/gen.js --env stage --api-flavor full", "gen:prod": "npm run gen:prod:default ; npm run gen:prod:full", "gen:prod:default": "node ./src/sdk/api/generator/gen.js", - "gen:prod:full": "node ./src/sdk/api/generator/gen.js --api-flavor full" + "gen:prod:full": "node ./src/sdk/api/generator/gen.js --api-flavor full", + "postinstall": "patch-package" }, "dependencies": { "@audius/anchor-audius-data": "0.0.2", @@ -43,7 +55,9 @@ "@certusone/wormhole-sdk": "0.1.1", "@ethersproject/solidity": "5.0.5", "@improbable-eng/grpc-web-node-http-transport": "0.15.0", + "@noble/secp256k1": "1.7.0", "@project-serum/anchor": "0.24.1", + "@scure/base": "1.1.1", "@solana/spl-token": "0.1.8", "@solana/web3.js": "1.37.1", "abi-decoder": "2.4.0", @@ -54,6 +68,8 @@ "borsh": "0.4.0", "bs58": "4.0.1", "cipher-base": "1.0.4", + "cross-fetch": "3.1.5", + "cuid": "2.1.8", "elliptic": "6.5.4", "esm": "3.2.25", "eth-sig-util": "2.5.4", @@ -69,6 +85,7 @@ "jsonschema": "1.2.6", "keccak256": "1.0.2", "lodash": "4.17.21", + "micro-aes-gcm": "0.3.3", "node-localstorage": "^1.3.1", "proper-url-join": "1.2.0", "rollup-plugin-shim": "^1.0.0", @@ -117,6 +134,7 @@ "mocha": "9.2.2", "nock": "13.1.2", "nyc": "15.1.0", + "patch-package": "6.5.0", "prettier": "^2.6.1", "prettier-config-standard": "^5.0.0", "rollup": "2.70.1", diff --git a/libs/patches/micro-aes-gcm+0.3.3.patch b/libs/patches/micro-aes-gcm+0.3.3.patch new file mode 100644 index 00000000000..23019969e03 --- /dev/null +++ b/libs/patches/micro-aes-gcm+0.3.3.patch @@ -0,0 +1,27 @@ +diff --git a/node_modules/micro-aes-gcm/index.ts b/node_modules/micro-aes-gcm/index.ts +index 07c24fe..d6ca0f0 100644 +--- a/node_modules/micro-aes-gcm/index.ts ++++ b/node_modules/micro-aes-gcm/index.ts +@@ -4,7 +4,7 @@ import * as nodeCrypto from 'crypto'; + declare const self: Record | undefined; + const crypto = { + node: nodeCrypto, +- web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined, ++ web: typeof self === 'object' && 'crypto' in self ? self['crypto'] : undefined, + }; + + // Caching slows it down 2-3x +@@ -29,11 +29,11 @@ function hexToBytes(hex: string): Uint8Array { + function concatBytes(...arrays: Uint8Array[]): Uint8Array { + if (!arrays.every((arr) => arr instanceof Uint8Array)) + throw new Error('Uint8Array list expected'); +- if (arrays.length === 1) return arrays[0]; ++ if (arrays.length === 1) return arrays[0]!; + const length = arrays.reduce((a, arr) => a + arr.length, 0); + const result = new Uint8Array(length); + for (let i = 0, pad = 0; i < arrays.length; i++) { +- const arr = arrays[i]; ++ const arr = arrays[i]!; + result.set(arr, pad); + pad += arr.length; + } diff --git a/libs/rollup.config.js b/libs/rollup.config.js deleted file mode 100644 index 92fedff0acb..00000000000 --- a/libs/rollup.config.js +++ /dev/null @@ -1,187 +0,0 @@ -import commonjs from '@rollup/plugin-commonjs' -import babel from '@rollup/plugin-babel' -import json from '@rollup/plugin-json' -import resolve from '@rollup/plugin-node-resolve' -import typescript from '@rollup/plugin-typescript' -import { terser } from 'rollup-plugin-terser' -import nodePolyfills from 'rollup-plugin-polyfill-node' -import alias from '@rollup/plugin-alias' -import ignore from 'rollup-plugin-ignore' - -import pkg from './package.json' - -const extensions = ['.js', '.ts'] - -const external = [ - ...Object.keys(pkg.dependencies), - ...Object.keys(pkg.devDependencies), - 'ethers/lib/utils', - 'ethers/lib/index', - 'hashids/cjs' -] - -const pluginTypescript = typescript({ tsconfig: './tsconfig.json' }) - -const commonConfig = { - plugins: [ - resolve({ extensions, preferBuiltins: true }), - commonjs({ extensions }), - babel({ babelHelpers: 'bundled', extensions }), - json(), - pluginTypescript - ], - external -} - -// For the browser bundle, these need to be internal because they either: -// * contain deps that need to be polyfilled via `nodePolyfills` -// * are ignored via `ignore` -const browserInternal = [ - 'eth-sig-util', - 'ethereumjs-tx', - 'ethereumjs-util', - 'ethereumjs-wallet', - 'graceful-fs', - 'node-localstorage', - 'abi-decoder', - 'web3', - 'xmlhttprequest' -] - -const browserConfig = { - plugins: [ - ignore(['web3', 'graceful-fs', 'node-localstorage']), - resolve({ extensions, preferBuiltins: false }), - commonjs({ - extensions, - transformMixedEsModules: true - }), - alias({ - entries: [{ find: 'stream', replacement: 'stream-browserify' }] - }), - nodePolyfills(), - babel({ babelHelpers: 'bundled', extensions }), - json(), - pluginTypescript - ], - external: external.filter((dep) => !browserInternal.includes(dep)) -} - -const browserDistFileConfig = { - plugins: [ - ignore(['web3', 'graceful-fs', 'node-localstorage']), - resolve({ extensions, preferBuiltins: false, browser: true }), - commonjs({ - extensions, - transformMixedEsModules: true - }), - alias({ - entries: [{ find: 'stream', replacement: 'stream-browserify' }] - }), - nodePolyfills(), - babel({ - babelHelpers: 'runtime', - extensions, - plugins: ['@babel/plugin-transform-runtime'] - }), - json(), - pluginTypescript - ], - external: ['web3'] -} - -const browserLegacyConfig = { - plugins: [ - ignore(['web3', 'graceful-fs', 'node-localstorage']), - resolve({ extensions, preferBuiltins: true }), - commonjs({ extensions }), - alias({ - entries: [{ find: 'stream', replacement: 'stream-browserify' }] - }), - babel({ babelHelpers: 'bundled', extensions }), - json(), - pluginTypescript - ], - external -} - -export default [ - /** - * SDK - */ - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - ...commonConfig - }, - - /** - * SDK bundled for a browser environment (includes polyfills for node libraries) - * Does not include libs but does include polyfills - */ - { - input: 'src/sdk/index.ts', - output: [ - { file: 'dist/index.browser.cjs.js', format: 'cjs', sourcemap: true }, - { file: 'dist/index.browser.esm.js', format: 'es', sourcemap: true } - ], - ...browserConfig, - watch: process.env.WATCH_BROWSER != "false", - }, - - /** - * SDK bundled for prebuilt package file to be used in browser - * Does not include libs but does include polyfills and all deps/dev deps - */ - { - input: 'src/sdk/sdkBrowserDist.ts', - output: [ - { - file: 'dist/sdk.js', - globals: { - web3: 'window.Web3' - }, - format: 'iife', - esModule: false, - sourcemap: true, - plugins: [terser()] - } - ], - ...browserDistFileConfig, - watch: process.env.WATCH_BROWSER != "false", - }, - - /** - * Legacy bundle for a browser environment - * Includes libs but does not include polyfills - */ - { - input: 'src/legacy.ts', - output: [{ file: 'dist/legacy.js', format: 'cjs', sourcemap: true }], - ...browserLegacyConfig, - watch: process.env.WATCH_BROWSER != "false", - }, - - /** - * ReactNative bundle used for our mobile app - * Includes a modified version of AudiusLibs with solana dependencies removed - */ - { - input: 'src/native-libs.ts', - output: [{ file: 'dist/native-libs.js', format: 'es', sourcemap: true }], - ...browserLegacyConfig, - watch: process.env.WATCH_BROWSER != "false", - }, - - /** - * core (used for eager requests) - */ - { - input: 'src/core.ts', - output: [{ file: 'dist/core.js', format: 'es', sourcemap: true }], - ...commonConfig, - } -] diff --git a/libs/rollup.config.ts b/libs/rollup.config.ts new file mode 100644 index 00000000000..78a55f68358 --- /dev/null +++ b/libs/rollup.config.ts @@ -0,0 +1,282 @@ +import commonjs from '@rollup/plugin-commonjs' +import babel from '@rollup/plugin-babel' +import json from '@rollup/plugin-json' +import resolve from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' +import { terser } from 'rollup-plugin-terser' +import nodePolyfills from 'rollup-plugin-polyfill-node' +import alias from '@rollup/plugin-alias' +import ignore from 'rollup-plugin-ignore' + +import pkg from './package.json' + +const extensions = ['.js', '.ts'] + +const external = [ + ...Object.keys(pkg.dependencies), + ...Object.keys(pkg.devDependencies), + 'ethers/lib/utils', + 'ethers/lib/index', + 'hashids/cjs' +] + +const pluginTypescript = typescript({ tsconfig: './tsconfig.json' }) + +/** + * For the browser bundle, these need to be internal because they either: + * - contain deps that need to be polyfilled via `nodePolyfills` + * - are ignored via `ignore` + */ +const browserInternal = [ + 'eth-sig-util', + 'ethereumjs-tx', + 'ethereumjs-util', + 'ethereumjs-wallet', + 'graceful-fs', + 'node-localstorage', + 'abi-decoder', + 'web3', + 'xmlhttprequest' +] + +/** + * ES-only dependencies need inlining when outputting a Common JS bundle, + * as requiring ES modules from Common JS isn't supported. + * Alternatively, these modules could be imported using dynamic imports, + * but that would have other side effects and affect each bundle output + * vs only affecting Common JS outputs, and requires Rollup 3.0. + * + * TODO: Make a test to ensure we don't add external ES-only modules to Common JS output + * + * See: + * - https://nodejs.org/api/esm.html#interoperability-with-commonjs + * - https://github.com/rollup/plugins/issues/481#issuecomment-661622792 + * - https://github.com/rollup/rollup/pull/4647 (3.0 supports keeping dynamic imports) + */ +const commonJsInternal = ['micro-aes-gcm'] + +export const outputConfigs = { + /** + * SDK (and Libs) Node Package (Common JS) + * Used by the Audius Content Node Service and Identity Service + * - Includes libs + * - Makes external ES modules internal to prevent issues w/ using require() + */ + sdkConfigCjs: { + input: 'src/index.ts', + output: [{ file: pkg.main, format: 'cjs', sourcemap: true }], + plugins: [ + resolve({ extensions, preferBuiltins: true }), + commonjs({ extensions }), + babel({ babelHelpers: 'bundled', extensions }), + json(), + pluginTypescript + ], + external: external.filter((id) => !commonJsInternal.includes(id)) + }, + + /** + * SDK (and Libs) Node Package (ES Module) + * Used by third parties using ES Modules + * Could be used by Audius Content Node and Identity Service after moving those services to ES module + * - Includes libs + */ + sdkConfigEs: { + input: 'src/index.ts', + output: [{ file: pkg.module, format: 'es', sourcemap: true }], + plugins: [ + resolve({ extensions, preferBuiltins: true }), + commonjs({ extensions }), + babel({ babelHelpers: 'bundled', extensions }), + json(), + pluginTypescript + ], + external + }, + + /** + * SDK React Native Package + * Used by the Audius React Native client + */ + sdkConfigReactNative: { + input: 'src/sdk/index.ts', + output: [{ file: pkg['react-native'], format: 'es', sourcemap: true }], + plugins: [ + ignore(['web3', 'graceful-fs', 'node-localstorage']), + resolve({ extensions, preferBuiltins: true }), + commonjs({ extensions }), + alias({ + entries: [{ find: 'stream', replacement: 'stream-browserify' }] + }), + babel({ babelHelpers: 'bundled', extensions, plugins: [] }), + json(), + pluginTypescript + ], + external + }, + + /** + * SDK Browser Package (Common JS) + * Possibly used by third parties + * - Includes polyfills for node libraries + * - Includes deps that are ignored or polyfilled for browser + * - Makes external ES modules internal to prevent issues w/ using require() + */ + sdkBrowserConfigCjs: { + input: 'src/sdk/index.ts', + output: [ + { file: 'dist/index.browser.cjs.js', format: 'cjs', sourcemap: true } + ], + plugins: [ + ignore(['web3', 'graceful-fs', 'node-localstorage']), + resolve({ extensions, preferBuiltins: false }), + commonjs({ + extensions, + transformMixedEsModules: true + }), + alias({ + entries: [{ find: 'stream', replacement: 'stream-browserify' }] + }), + nodePolyfills(), + babel({ babelHelpers: 'bundled', extensions }), + json(), + pluginTypescript + ], + external: external.filter( + (dep) => !browserInternal.includes(dep) && !commonJsInternal.includes(dep) + ) + }, + + /** + * SDK Browser Package (ES Module) + * Used by the Audius Web Client and by extension the Desktop Client + * - Includes polyfills for node libraries + * - Includes deps that are ignored or polyfilled for browser + */ + sdkBrowserConfigEs: { + input: 'src/sdk/index.ts', + output: [ + { file: 'dist/index.browser.esm.js', format: 'es', sourcemap: true } + ], + plugins: [ + ignore(['web3', 'graceful-fs', 'node-localstorage']), + resolve({ extensions, preferBuiltins: false }), + commonjs({ + extensions, + transformMixedEsModules: true + }), + alias({ + entries: [{ find: 'stream', replacement: 'stream-browserify' }] + }), + nodePolyfills(), + babel({ babelHelpers: 'bundled', extensions }), + json(), + pluginTypescript + ], + external: external.filter((dep) => !browserInternal.includes(dep)) + }, + + /** + * SDK Browser Distributable + * Meant to be used directly in the browser without any module resolver + * - Includes polyfills for node libraries + * - Includes all deps/dev deps except web3 + */ + sdkBrowserDistConfig: { + input: 'src/sdk/sdkBrowserDist.ts', + output: [ + { + file: 'dist/sdk.js', + globals: { + web3: 'window.Web3' + }, + format: 'iife', + esModule: false, + sourcemap: true, + plugins: [terser()] + } + ], + plugins: [ + ignore(['web3', 'graceful-fs', 'node-localstorage']), + resolve({ extensions, preferBuiltins: false, browser: true }), + commonjs({ + extensions, + transformMixedEsModules: true + }), + alias({ + entries: [{ find: 'stream', replacement: 'stream-browserify' }] + }), + nodePolyfills(), + babel({ + babelHelpers: 'runtime', + extensions, + plugins: ['@babel/plugin-transform-runtime'] + }), + json(), + pluginTypescript + ], + external: ['web3'] + }, + + /** + * Libs Legacy Browser Package + * Used by the Audius Web Client and by extension the Desktop Client + */ + legacyBrowserConfig: { + input: 'src/legacy.ts', + output: [{ file: 'dist/legacy.js', format: 'cjs', sourcemap: true }], + plugins: [ + ignore(['web3', 'graceful-fs', 'node-localstorage']), + resolve({ extensions, preferBuiltins: true }), + commonjs({ extensions }), + alias({ + entries: [{ find: 'stream', replacement: 'stream-browserify' }] + }), + babel({ babelHelpers: 'bundled', extensions }), + json(), + pluginTypescript + ], + external + }, + + /** + * Libs Legacy React Native Package + * Used by the Audius React Native Client + * - Includes a modified version of AudiusLibs with Solana dependencies removed + */ + legacyReactNativeConfig: { + input: 'src/native-libs.ts', + output: [{ file: 'dist/native-libs.js', format: 'es', sourcemap: true }], + plugins: [ + ignore(['web3', 'graceful-fs', 'node-localstorage']), + resolve({ extensions, preferBuiltins: true }), + commonjs({ extensions }), + alias({ + entries: [{ find: 'stream', replacement: 'stream-browserify' }] + }), + babel({ babelHelpers: 'bundled', extensions }), + json(), + pluginTypescript + ], + external + }, + + /** + * Core Package + * Exports a small bundle that can be loaded quickly, useful for eager requests + */ + coreConfig: { + input: 'src/core.ts', + output: [{ file: 'dist/core.js', format: 'es', sourcemap: true }], + plugins: [ + resolve({ extensions, preferBuiltins: true }), + commonjs({ extensions }), + babel({ babelHelpers: 'bundled', extensions }), + json(), + pluginTypescript + ], + external + } +} + +export default Object.values(outputConfigs) diff --git a/libs/rollup.legacy.browser.config.ts b/libs/rollup.legacy.browser.config.ts new file mode 100644 index 00000000000..89d06e09a18 --- /dev/null +++ b/libs/rollup.legacy.browser.config.ts @@ -0,0 +1,3 @@ +import { outputConfigs } from './rollup.config' + +export default [outputConfigs.legacyBrowserConfig] diff --git a/libs/rollup.legacy.native.config.ts b/libs/rollup.legacy.native.config.ts new file mode 100644 index 00000000000..f159edd6976 --- /dev/null +++ b/libs/rollup.legacy.native.config.ts @@ -0,0 +1,3 @@ +import { outputConfigs } from './rollup.config' + +export default [outputConfigs.legacyReactNativeConfig] diff --git a/libs/rollup.sdk.browser.config.ts b/libs/rollup.sdk.browser.config.ts new file mode 100644 index 00000000000..d69d3ecfc9a --- /dev/null +++ b/libs/rollup.sdk.browser.config.ts @@ -0,0 +1,6 @@ +import { outputConfigs } from './rollup.config' + +export default [ + outputConfigs.sdkBrowserConfigEs, + outputConfigs.sdkBrowserConfigCjs +] diff --git a/libs/rollup.sdk.config.ts b/libs/rollup.sdk.config.ts new file mode 100644 index 00000000000..22e980e4204 --- /dev/null +++ b/libs/rollup.sdk.config.ts @@ -0,0 +1,3 @@ +import { outputConfigs } from './rollup.config' + +export default [outputConfigs.sdkConfigEs, outputConfigs.sdkConfigCjs] diff --git a/libs/rollup.sdk.native.config.ts b/libs/rollup.sdk.native.config.ts new file mode 100644 index 00000000000..0aa7984816e --- /dev/null +++ b/libs/rollup.sdk.native.config.ts @@ -0,0 +1,3 @@ +import { outputConfigs } from './rollup.config' + +export default [outputConfigs.sdkConfigReactNative] diff --git a/libs/rollup.sdkbrowser.config.js b/libs/rollup.sdkbrowser.config.js new file mode 100644 index 00000000000..a87bc6f7de9 --- /dev/null +++ b/libs/rollup.sdkbrowser.config.js @@ -0,0 +1,3 @@ +import { outputConfigs } from './rollup.config' + +export default [outputConfigs.browserSdkConfigPolyfill] diff --git a/libs/src/sdk/api/HealthCheckResponseData.ts b/libs/src/sdk/api/HealthCheckResponseData.ts new file mode 100644 index 00000000000..7ceb95a7b4c --- /dev/null +++ b/libs/src/sdk/api/HealthCheckResponseData.ts @@ -0,0 +1,135 @@ +export type HealthCheckResponseData = Partial<{ + auto_upgrade_enabled: boolean + block_difference: number + challenge_last_event_age_sec: number + database_connections: number + database_is_localhost: boolean + database_size: number + db: { + blockhash: string + number: number + } + filesystem_size: number + filesystem_used: number + final_poa_block: any + git: string + index_eth_age_sec: number + infra_setup: string + last_scanned_block_for_balance_refresh: number + last_track_unavailability_job_end_time: string + last_track_unavailability_job_start_time: string + latest_block_num: number + latest_indexed_block_num: number + maximum_healthy_block_difference: number + meets_min_requirements: boolean + num_users_in_immediate_balance_refresh_queue: number + num_users_in_lazy_balance_refresh_queue: number + number_of_cpus: number + openresty_public_key: string + plays: { + is_unhealthy: boolean + oldest_unarchived_play_created_at: string + time_diff_general: number + tx_info: { + slot_diff: number + time_diff: number + tx_info: { + chain_tx: { + signature: string + slot: number + timestamp: number + } + db_tx: { + signature: string + slot: number + timestamp: number + } + } + } + } + reactions: { + indexing_delta: number + is_unhealthy: boolean + reaction_delta: number + } + received_bytes_per_sec: number + redis_total_memory: number + rewards_manager: { + is_unhealthy: number + time_diff_general: number + tx_info: { + slot_diff: number + time_diff: number + tx_info: { + chain_tx: { + signature: string + slot: number + timestamp: number + } + db_tx: { + signature: string + slot: number + timestamp: number + } + } + } + } + service: string + spl_audio_info: { + is_unhealthy: boolean + time_diff_general: number + tx_info: { + slot_diff: number + time_diff: number + tx_info: { + chain_tx: { + signature: string + slot: number + timestamp: number + } + db_tx: { + signature: string + slot: number + timestamp: number + } + } + } + } + total_memory: number + transactions_history_backfill: { + rewards_manager_backfilling_complete: boolean + spl_token_backfilling_complete: boolean + user_bank_backfilling_complete: boolean + } + transferred_bytes_per_sec: number + trending_playlists_age_sec: number + trending_tracks_age_sec: number + url: string + used_memory: number + user_balances_age_sec: number + user_bank: { + is_unhealthy: boolean + time_diff_general: number + tx_info: { + slot_diff: number + time_diff: number + tx_info: { + chain_tx: { + signature: string + slot: number + timestamp: number + } + db_tx: { + signature: number + slot: string + timestamp: number + } + } + } + } + version: string + web: { + blockhash: string + blocknumber: number + } +}> diff --git a/libs/src/sdk/api/chats/ChatsApi.ts b/libs/src/sdk/api/chats/ChatsApi.ts new file mode 100644 index 00000000000..51cda1e12ca --- /dev/null +++ b/libs/src/sdk/api/chats/ChatsApi.ts @@ -0,0 +1,430 @@ +import { + BaseAPI, + Configuration, + RequiredError, + WalletAPI +} from '../generated/default' +import * as aes from 'micro-aes-gcm' +import { base64 } from '@scure/base' +import cuid from 'cuid' + +import * as secp from '@noble/secp256k1' +import type { + RPCPayload, + ChatInvite, + UserChat, + ChatMessage +} from './serverTypes' +import type { + ChatBlockRequest, + ChatCreateRequest, + ChatDeleteRequest, + ChatGetAllRequest, + ChatGetMessagesRequest, + ChatGetRequest, + ChatInviteRequest, + ChatMessageRequest, + ChatPermitRequest, + ChatReactRequest, + ChatReadRequest, + TypedCommsResponse +} from './clientTypes' + +export class ChatsApi extends BaseAPI { + private chatSecrets: Record = {} + private readonly walletApi: WalletAPI + + constructor(params: Configuration) { + super(params) + this.assertNotNullOrUndefined( + params.walletApi, + 'params.walletApi', + 'constructor' + ) + this.walletApi = params.walletApi! + } + + // #region QUERY + + public async get(requestParameters: ChatGetRequest) { + this.assertNotNullOrUndefined( + requestParameters.chatId, + 'requestParameters.chatId', + 'getChat' + ) + const path = `/comms/chats/${requestParameters.chatId}` + return (await this.request({ + method: 'GET', + path, + headers: await this.getSignatureHeader(path) + })) as TypedCommsResponse + } + + public async getAll(requestParameters?: ChatGetAllRequest) { + const path = `/comms/chats` + const queryParameters: any = {} + if (requestParameters?.limit) { + queryParameters.limit = requestParameters.limit + } + if (requestParameters?.cursor) { + queryParameters.offset = requestParameters.cursor + } + return (await this.request({ + method: 'GET', + path, + headers: await this.getSignatureHeader(path), + query: queryParameters + })) as TypedCommsResponse + } + + public async getMessages( + requestParameters: ChatGetMessagesRequest + ): Promise> { + this.assertNotNullOrUndefined( + requestParameters.chatId, + 'requestParameters.chatId', + 'getMessages' + ) + + const sharedSecret = await this.getChatSecret(requestParameters.chatId) + const path = `/comms/chats/${requestParameters.chatId}/messages` + const queryParameters: any = {} + if (requestParameters.limit) { + queryParameters.limit = requestParameters.limit + } + if (requestParameters.before) { + queryParameters.before = requestParameters.before + } + if (requestParameters.after) { + queryParameters.after = requestParameters.after + } + const response = (await this.request({ + method: 'GET', + path, + headers: await this.getSignatureHeader(path), + query: queryParameters + })) as TypedCommsResponse + const unencrypted = await Promise.all( + response.data.map(async (m) => ({ + ...m, + message: await this.decryptString( + sharedSecret, + base64.decode(m.message) + ) + })) + ) + return { + ...response, + data: unencrypted + } + } + + // #endregion + + // #region MUTATE + + public async create(requestParameters: ChatCreateRequest) { + this.assertNotNullOrUndefined( + requestParameters.userId, + 'requestParameters.userId', + 'create' + ) + this.assertNotNullOrUndefined( + requestParameters.invitedUserIds, + 'requestParameters.invitedUserIds', + 'create' + ) + this.assertMinLength( + requestParameters.invitedUserIds, + 'requestParameters.invitedUserIds', + 'create' + ) + + const chatId = cuid() + const chatSecret = secp.utils.randomPrivateKey() + this.chatSecrets[chatId] = chatSecret + const invites = await this.createInvites( + requestParameters.userId, + requestParameters.invitedUserIds, + chatSecret + ) + + return await this.sendRpc({ + method: 'chat.create', + params: { + chat_id: chatId, + invites + } + }) + } + + public async invite(requestParameters: ChatInviteRequest) { + this.assertNotNullOrUndefined( + requestParameters.chatId, + 'requestParameters.chatId', + 'invite' + ) + this.assertNotNullOrUndefined( + requestParameters.userId, + 'requestParameters.userId', + 'invite' + ) + this.assertNotNullOrUndefined( + requestParameters.invitedUserIds, + 'requestParameters.invitedUserIds', + 'invite' + ) + this.assertMinLength( + requestParameters.invitedUserIds, + 'requestParameters.invitedUserIds', + 'invite' + ) + + const chatSecret = await this.getChatSecret(requestParameters.chatId) + const invites = await this.createInvites( + requestParameters.userId, + requestParameters.invitedUserIds, + chatSecret + ) + return await this.sendRpc({ + method: 'chat.invite', + params: { + chat_id: requestParameters.chatId, + invites + } + }) + } + + public async message(requestParameters: ChatMessageRequest) { + this.assertNotNullOrUndefined( + requestParameters.chatId, + 'requestParameters.chatId', + 'message' + ) + this.assertNotNullOrUndefined( + requestParameters.message, + 'requestParameters.message', + 'message' + ) + + const chatSecret = await this.getChatSecret(requestParameters.chatId) + const encrypted = await this.encryptString( + chatSecret, + requestParameters.message + ) + const message = base64.encode(encrypted) + + return await this.sendRpc({ + method: 'chat.message', + params: { + chat_id: requestParameters.chatId, + message_id: cuid(), + message + } + }) + } + + public async react(requestParameters: ChatReactRequest) { + this.assertNotNullOrUndefined( + requestParameters.messageId, + 'requestParameters.messageId', + 'react' + ) + this.assertNotNullOrUndefined( + requestParameters.reaction, + 'requestParameters.reaction', + 'react' + ) + return await this.sendRpc({ + method: 'chat.react', + params: { + message_id: requestParameters.messageId, + reaction: requestParameters.reaction + } + }) + } + + public async read(requestParameters: ChatReadRequest) { + this.assertNotNullOrUndefined( + requestParameters.chatId, + 'requestParameters.chatId', + 'read' + ) + return await this.sendRpc({ + method: 'chat.read', + params: { + chat_id: requestParameters.chatId + } + }) + } + + public async block(requestParameters: ChatBlockRequest) { + this.assertNotNullOrUndefined( + requestParameters.userId, + 'requestParameters.userId', + 'block' + ) + return await this.sendRpc({ + method: 'chat.block', + params: { + user_id: requestParameters.userId + } + }) + } + + public async delete(requestParameters: ChatDeleteRequest) { + this.assertNotNullOrUndefined( + requestParameters.chatId, + 'requestParameters.chatId', + 'delete' + ) + return await this.sendRpc({ + method: 'chat.delete', + params: { + chat_id: requestParameters.chatId + } + }) + } + + public async permit(requestParameters: ChatPermitRequest) { + this.assertNotNullOrUndefined( + requestParameters.permit, + 'requestParameters.permit', + 'permit' + ) + return await this.sendRpc({ + method: 'chat.permit', + params: { + permit: requestParameters.permit + } + }) + } + + // #endregion + + // #region PRIVATE + + private assertNotNullOrUndefined(value: any, name: string, method: string) { + if (value === null || value === undefined) { + throw new RequiredError( + name, + `Required parameter ${name} was null or undefined when calling ${method}.` + ) + } + } + + private assertMinLength( + value: any[], + name: string, + method: string, + minimumLength: number = 1 + ) { + if (value.length < minimumLength) { + throw new RequiredError( + name, + `Required parameter ${name} requires more than ${minimumLength} element when calling ${method}` + ) + } + } + + private async createInvites( + userId: string, + invitedUserIds: string[], + chatSecret: Uint8Array + ): Promise { + const userPublicKey = await this.getPublicKey(userId) + return await Promise.all( + [userId, ...invitedUserIds].map(async (userId) => { + const inviteePublicKey = await this.getPublicKey(userId) + const inviteCode = await this.createInviteCode( + userPublicKey, + inviteePublicKey, + chatSecret + ) + return { + user_id: userId, + invite_code: base64.encode(inviteCode) + } + }) + ) + } + + private async createInviteCode( + userPublicKey: Uint8Array, + inviteePublicKey: Uint8Array, + chatSecret: Uint8Array + ) { + const sharedSecret = await this.walletApi.getSharedSecret(inviteePublicKey) + const encryptedChatSecret = await this.encrypt(sharedSecret, chatSecret) + const inviteCode = new Uint8Array(65 + encryptedChatSecret.length) + inviteCode.set(userPublicKey) + inviteCode.set(encryptedChatSecret, 65) + return inviteCode + } + + private async readInviteCode(inviteCode: Uint8Array) { + const friendPublicKey = inviteCode.slice(0, 65) + const chatSecretEncrypted = inviteCode.slice(65) + const sharedSecret = await this.walletApi.getSharedSecret(friendPublicKey) + return await this.decrypt(sharedSecret, chatSecretEncrypted) + } + + private async encrypt(secret: Uint8Array, payload: Uint8Array) { + return await aes.encrypt(secret.slice(secret.length - 32), payload) + } + + private async encryptString(secret: Uint8Array, payload: string) { + return await this.encrypt(secret, new TextEncoder().encode(payload)) + } + + private async decrypt(secret: Uint8Array, payload: Uint8Array) { + return await aes.decrypt(secret.slice(secret.length - 32), payload) + } + + private async decryptString(secret: Uint8Array, payload: Uint8Array) { + return new TextDecoder().decode(await this.decrypt(secret, payload)) + } + + private async getChatSecret(chatId: string) { + const existingChatSecret = this.chatSecrets[chatId] + if (!existingChatSecret) { + const response = await this.get({ chatId }) + const chatSecret = await this.readInviteCode( + base64.decode(response.data.invite_code) + ) + this.chatSecrets[chatId] = chatSecret + return chatSecret + } + return existingChatSecret + } + + private async getPublicKey(userId: string) { + const response = await this.request({ + path: `/comms/pubkey/${userId}`, + method: 'GET', + headers: {} + }) + return base64.decode(response.data) + } + + private async getSignatureHeader(payload: string) { + const [allSignatureBytes, recoveryByte] = await this.walletApi.sign(payload) + const signatureBytes = new Uint8Array(65) + signatureBytes.set(allSignatureBytes, 0) + signatureBytes[64] = recoveryByte + return { 'x-sig': base64.encode(signatureBytes) } + } + + private async sendRpc(args: RPCPayload) { + const payload = JSON.stringify(args) + await this.request({ + path: `/comms/mutate`, + method: 'POST', + headers: await this.getSignatureHeader(payload), + body: payload + }) + return args + } + + // #endregion +} diff --git a/libs/src/sdk/api/chats/clientTypes.ts b/libs/src/sdk/api/chats/clientTypes.ts new file mode 100644 index 00000000000..adfd42b19eb --- /dev/null +++ b/libs/src/sdk/api/chats/clientTypes.ts @@ -0,0 +1,61 @@ +import type { CommsResponse, ChatPermission } from './serverTypes' + +// REQUEST PARAMETERS + +export type ChatGetAllRequest = { + limit?: number + cursor?: string +} + +export type ChatGetRequest = { + chatId: string +} + +export type ChatGetMessagesRequest = { + chatId: string + limit?: number + before?: string + after?: string +} + +export type ChatCreateRequest = { + chatId: string + userId: string + invitedUserIds: string[] +} + +export type ChatInviteRequest = { + chatId: string + userId: string + invitedUserIds: string[] +} + +export type ChatMessageRequest = { + chatId: string + message: string +} + +export type ChatReactRequest = { + messageId: string + reaction: string +} + +export type ChatReadRequest = { + chatId: string +} + +export type ChatBlockRequest = { + userId: string +} + +export type ChatDeleteRequest = { + chatId: string +} + +export type ChatPermitRequest = { + permit: ChatPermission +} + +export type TypedCommsResponse = Omit & { + data: T +} diff --git a/libs/src/sdk/api/chats/serverTypes.ts b/libs/src/sdk/api/chats/serverTypes.ts new file mode 100644 index 00000000000..5f5e4f867e4 --- /dev/null +++ b/libs/src/sdk/api/chats/serverTypes.ts @@ -0,0 +1,155 @@ +// NOTE: No imports allowed - quicktype is not yet able to track imports! + +export type ChatCreateRPC = { + method: 'chat.create' + params: { + chat_id: string + invites: Array<{ + user_id: string + invite_code: string + }> + } +} + +export type ChatDeleteRPC = { + method: 'chat.delete' + params: { + chat_id: string + } +} + +export type ChatInviteRPC = { + method: 'chat.invite' + params: { + chat_id: string + invites: Array<{ + user_id: string + invite_code: string + }> + } +} + +export type ChatMessageRPC = { + method: 'chat.message' + params: { + chat_id: string + message_id: string + message: string + parent_message_id?: string + } +} + +export type ChatReactRPC = { + method: 'chat.react' + params: { + message_id: string + reaction: string + } +} + +export type ChatReadRPC = { + method: 'chat.read' + params: { + chat_id: string + } +} + +export type ChatBlockRPC = { + method: 'chat.block' + params: { + user_id: string + } +} + +export type ChatUnblockRPC = { + method: 'chat.unblock' + params: { + user_id: string + } +} + +export type ChatPermitRPC = { + method: 'chat.permit' + params: { + permit: ChatPermission + } +} + +export type RPCPayload = + | ChatCreateRPC + | ChatDeleteRPC + | ChatInviteRPC + | ChatMessageRPC + | ChatReactRPC + | ChatReadRPC + | ChatBlockRPC + | ChatUnblockRPC + | ChatPermitRPC + +export type RPCMethod = RPCPayload['method'] + +export type UserChat = { + // User agnostic + chat_id: string + last_message_at: string + chat_members: Array<{ user_id: string }> + + // User specific + invite_code: string + unread_message_count: number + last_read_at: string +} + +export type ChatMessage = { + message_id: string + sender_user_id: string + created_at: string + message: string + reactions: Array<{ + user_id: string + created_at: string + reaction: string + }> +} + +export type ChatInvite = { + user_id: string + invite_code: string +} + +/** + * Defines who the user allows to message them + */ +export enum ChatPermission { + /** + * Messages are allowed for everyone + */ + ALL = 'all', + /** + * Messages are only allowed for users that have tipped me + */ + TIPPERS = 'tippers', + /** + * Messages are only allowed for users I follow + */ + FOLLOWEES = 'followees', + /** + * Messages are not allowed + */ + NONE = 'none' +} + +export type CommsResponse = { + health: { + is_healthy: boolean + } + summary?: { + prev_cursor: string + next_cursor: string + remaining_count: number + total_count: number + } + // Overridden in client types but left as any for the server. + // quicktype/golang doesn't do well with union types + data: any +} diff --git a/libs/src/sdk/api/generated/default/runtime.ts b/libs/src/sdk/api/generated/default/runtime.ts index 1cd2941395b..633e2e14c3e 100644 --- a/libs/src/sdk/api/generated/default/runtime.ts +++ b/libs/src/sdk/api/generated/default/runtime.ts @@ -79,7 +79,7 @@ export class BaseAPI { // do not handle correctly sometimes. url += '?' + this.configuration.queryParamsStringify(context.query); } - const body = ((typeof FormData !== "undefined" && context.body instanceof FormData) || context.body instanceof URLSearchParams || isBlob(context.body)) + const body = ((typeof FormData !== "undefined" && context.body instanceof FormData) || context.body instanceof URLSearchParams || isBlob(context.body) || typeof context.body === 'string') ? context.body : JSON.stringify(context.body); @@ -153,6 +153,12 @@ export const COLLECTION_FORMATS = { // Returns unknown and is cast to the appropriate type in the corresponding api method export type FetchAPI = (url: string, init?: RequestInit) => Promise +// Injected helper methods for methods requiring user signatures or encryption +export type WalletAPI = { + getSharedSecret: (publicKey: string | Uint8Array) => Promise + sign: (data: string) => Promise<[Uint8Array, number]> +} + export interface ConfigurationParameters { basePath?: string; // override base path fetchApi: FetchAPI; // fetch implementation @@ -164,6 +170,7 @@ export interface ConfigurationParameters { accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security headers?: HTTPHeaders; //header params we want to use on every request credentials?: RequestCredentials; //value for the credentials param we want to use on each request + walletApi: WalletAPI } export class Configuration { @@ -216,6 +223,10 @@ export class Configuration { get credentials(): RequestCredentials | undefined { return this.configuration.credentials; } + + get walletApi(): WalletAPI { + return this.configuration.walletApi; + } } /** diff --git a/libs/src/sdk/api/generated/full/runtime.ts b/libs/src/sdk/api/generated/full/runtime.ts index bcc9f78dc8f..15a7120db55 100644 --- a/libs/src/sdk/api/generated/full/runtime.ts +++ b/libs/src/sdk/api/generated/full/runtime.ts @@ -79,7 +79,7 @@ export class BaseAPI { // do not handle correctly sometimes. url += '?' + this.configuration.queryParamsStringify(context.query); } - const body = ((typeof FormData !== "undefined" && context.body instanceof FormData) || context.body instanceof URLSearchParams || isBlob(context.body)) + const body = ((typeof FormData !== "undefined" && context.body instanceof FormData) || context.body instanceof URLSearchParams || isBlob(context.body) || typeof context.body === 'string') ? context.body : JSON.stringify(context.body); @@ -153,6 +153,12 @@ export const COLLECTION_FORMATS = { // Returns unknown and is cast to the appropriate type in the corresponding api method export type FetchAPI = (url: string, init?: RequestInit) => Promise +// Injected helper methods for methods requiring user signatures or encryption +export type WalletAPI = { + getSharedSecret: (publicKey: string | Uint8Array) => Promise + sign: (data: string) => Promise<[Uint8Array, number]> +} + export interface ConfigurationParameters { basePath?: string; // override base path fetchApi: FetchAPI; // fetch implementation @@ -164,6 +170,7 @@ export interface ConfigurationParameters { accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security headers?: HTTPHeaders; //header params we want to use on every request credentials?: RequestCredentials; //value for the credentials param we want to use on each request + walletApi: WalletAPI } export class Configuration { @@ -216,6 +223,10 @@ export class Configuration { get credentials(): RequestCredentials | undefined { return this.configuration.credentials; } + + get walletApi(): WalletAPI { + return this.configuration.walletApi; + } } /** diff --git a/libs/src/sdk/api/generator/templates/typescript-fetch/runtime.mustache b/libs/src/sdk/api/generator/templates/typescript-fetch/runtime.mustache index f043bb23eca..310fc1dfe8f 100644 --- a/libs/src/sdk/api/generator/templates/typescript-fetch/runtime.mustache +++ b/libs/src/sdk/api/generator/templates/typescript-fetch/runtime.mustache @@ -68,7 +68,7 @@ export class BaseAPI { // do not handle correctly sometimes. url += '?' + this.configuration.queryParamsStringify(context.query); } - const body = ((typeof FormData !== "undefined" && context.body instanceof FormData) || context.body instanceof URLSearchParams || isBlob(context.body)) + const body = ((typeof FormData !== "undefined" && context.body instanceof FormData) || context.body instanceof URLSearchParams || isBlob(context.body) || typeof context.body === 'string') ? context.body : JSON.stringify(context.body); @@ -142,6 +142,12 @@ export const COLLECTION_FORMATS = { // Returns unknown and is cast to the appropriate type in the corresponding api method export type FetchAPI = (url: string, init?: RequestInit) => Promise +// Injected helper methods for methods requiring user signatures or encryption +export type WalletAPI = { + getSharedSecret: (publicKey: string | Uint8Array) => Promise + sign: (data: string) => Promise<[Uint8Array, number]> +} + export interface ConfigurationParameters { basePath?: string; // override base path fetchApi: FetchAPI; // fetch implementation @@ -153,6 +159,7 @@ export interface ConfigurationParameters { accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security headers?: HTTPHeaders; //header params we want to use on every request credentials?: RequestCredentials; //value for the credentials param we want to use on each request + walletApi: WalletAPI } export class Configuration { @@ -205,6 +212,10 @@ export class Configuration { get credentials(): RequestCredentials | undefined { return this.configuration.credentials; } + + get walletApi(): WalletAPI { + return this.configuration.walletApi; + } } /** diff --git a/libs/src/sdk/index.ts b/libs/src/sdk/index.ts index 93935e34245..1510dd7325d 100644 --- a/libs/src/sdk/index.ts +++ b/libs/src/sdk/index.ts @@ -3,3 +3,9 @@ export * as full from './api/generated/full' export * from './api/generated/default' export { TracksApi } from './api/TracksApi' export { ResolveApi } from './api/ResolveApi' +export { + GetAudioTransactionHistorySortMethodEnum, + GetAudioTransactionHistorySortDirectionEnum +} from './api/generated/full' +export * from './api/chats/clientTypes' +export * from './api/chats/serverTypes' diff --git a/libs/src/sdk/middleware/addAppNameMiddleware.ts b/libs/src/sdk/middleware/addAppNameMiddleware.ts new file mode 100644 index 00000000000..d14eeea0750 --- /dev/null +++ b/libs/src/sdk/middleware/addAppNameMiddleware.ts @@ -0,0 +1,27 @@ +import { + type Middleware, + type RequestContext, + type FetchParams, + querystring +} from '../api/generated/default' + +/** + * Appends the configured app_name to the query string for tracking API usage + * @param options the middleware options + * @param {string} options.appName the name of the app using the SDK + */ +export const addAppNameMiddleware = ({ + appName +}: { + appName: string +}): Middleware => { + return { + pre: async (context: RequestContext): Promise => ({ + url: + context.url + + (context.url.includes('?') ? '&' : '?') + + querystring({ app_name: appName }), + init: context.init ?? {} + }) + } +} diff --git a/libs/src/sdk/middleware/discoveryNodeSelectorMiddleware.ts b/libs/src/sdk/middleware/discoveryNodeSelectorMiddleware.ts new file mode 100644 index 00000000000..d87d9d19ca2 --- /dev/null +++ b/libs/src/sdk/middleware/discoveryNodeSelectorMiddleware.ts @@ -0,0 +1,257 @@ +import type { + Middleware, + RequestContext, + ResponseContext +} from '../api/generated/default' +import { DISCOVERY_SERVICE_NAME } from '../../services/discoveryProvider/constants' +import type { DiscoveryProviderSelection } from '../../services/discoveryProvider/DiscoveryProviderSelection' +import fetch from 'cross-fetch' +import semver from 'semver' +import type { HealthCheckResponseData } from '../api/HealthCheckResponseData' + +const isSolanaIndexerHealthy = ({ + data, + unhealthySlotDiffPlays +}: { + data: HealthCheckResponseData + unhealthySlotDiffPlays: number | null +}) => { + return ( + !data.plays?.is_unhealthy && + !data.rewards_manager?.is_unhealthy && + !data.spl_audio_info?.is_unhealthy && + !data.user_bank?.is_unhealthy && + (!data.plays?.tx_info?.slot_diff || + unhealthySlotDiffPlays === null || + data.plays?.tx_info?.slot_diff < unhealthySlotDiffPlays) + ) +} + +const isApiResponseHealthy = ({ + data, + endpoint, + currentVersion, + unhealthyBlockDiff, + unhealthySlotDiffPlays +}: { + data: any + endpoint: string + currentVersion: string + unhealthyBlockDiff: number + unhealthySlotDiffPlays: number | null +}) => { + if ( + data.version?.service && + data.version.service !== DISCOVERY_SERVICE_NAME + ) { + console.warn('Audius SDK discovery provider service name unhealthy', { + endpoint + }) + return false + } + if ( + data.version?.version && + semver.lt(data.version.version, currentVersion) + ) { + console.warn('Audius SDK discovery provider version unhealthy', { + endpoint + }) + return false + } + if ( + data.latest_chain_block && + data.latest_indexed_block && + data.latest_chain_block - data.latest_indexed_block > unhealthyBlockDiff + ) { + console.warn('Audius SDK discovery provider POA indexing unhealthy', { + endpoint + }) + return false + } + if ( + unhealthySlotDiffPlays && + data.latest_chain_slot_plays && + data.latest_indexed_slot_plays && + data.latest_chain_slot_plays - data.latest_indexed_slot_plays > + unhealthySlotDiffPlays + ) { + console.warn('Audius SDK discovery provider Solana indexing unhealthy', { + endpoint + }) + return false + } + return true +} + +const isDiscoveryNodeHealthy = async ({ + endpoint, + currentVersion, + unhealthyBlockDiff, + unhealthySlotDiffPlays +}: { + endpoint: string + currentVersion: string + unhealthyBlockDiff: number + unhealthySlotDiffPlays: number | null +}) => { + const healthCheckURL = `${endpoint}/health_check` + let data = null + try { + // Don't use context.fetch to bypass middleware + const response = await fetch(healthCheckURL) + if (response.status !== 200) { + throw new Error() + } + const json = await response.json() + data = json.data as HealthCheckResponseData + if (!data) { + throw new Error() + } + } catch { + console.warn('Audius SDK discovery provider health_check unhealthy', { + endpoint + }) + return false + } + if (data.service !== DISCOVERY_SERVICE_NAME) { + console.warn('Audius SDK discovery provider service name unhealthy', { + endpoint + }) + return false + } + if (!data.version || semver.lt(data.version, currentVersion)) { + console.warn('Audius SDK discovery provider version unhealthy', { + endpoint + }) + return false + } + if (!data.block_difference || data.block_difference > unhealthyBlockDiff) { + console.warn('Audius SDK discovery provider POA indexing unhealthy', { + endpoint + }) + return false + } + if (!isSolanaIndexerHealthy({ data, unhealthySlotDiffPlays })) { + console.warn('Audius SDK discovery provider Solana indexing unhealthy', { + endpoint + }) + return false + } + + return true +} + +const reselectAndRetry = async ({ + endpoint, + discoveryProviderSelector, + context +}: { + endpoint: string + discoveryProviderSelector: DiscoveryProviderSelection + context: ResponseContext +}) => { + const path = context.url.substring(endpoint.length) + discoveryProviderSelector.addUnhealthy(endpoint) + discoveryProviderSelector.clearCached() + const newSelectionPromise = discoveryProviderSelector.select() + const newEndpoint = await newSelectionPromise + console.warn( + 'Audius SDK discovery provider endpoint unhealthy, reselected discovery provider and retrying:', + { + endpoint, + newEndpoint + } + ) + // Don't use context.fetch to bypass middleware + const response = await fetch(`${newEndpoint}${path}`, context.init) + return { newSelectionPromise, response } +} + +/** + * Uses a discovery provider selector to select a discovery node + * and prepends the request URL with the discovery provider endpoint. + * - On successful requests, checks the _response body_ to make sure the currently selected endpoint is still healthy, + * and if not, selects a new discovery provider and retries once. + * - On failed requests, checks the _health check endpoint_ to make sure the currently selected endpoint is still healthy, + * and if not, selects a new discovery provider and retries once. + * @param options the middleware options + * @param {DiscoveryProviderSelection} options.discoveryProviderSelector - the DiscoveryProviderSelection instance to use to select a discovery provider + */ +export const discoveryNodeSelectorMiddleware = ({ + discoveryProviderSelector +}: { + discoveryProviderSelector: DiscoveryProviderSelection +}): Middleware => { + let selectionPromise = discoveryProviderSelector.select() as Promise< + string | undefined + > + return { + pre: async (context: RequestContext) => { + // Select discovery using service selection method + const endpoint = await selectionPromise + if (!endpoint) { + throw new Error( + 'All Discovery Providers are unhealthy and unavailable.' + ) + } + // Prepend discovery endpoint to url + return { + url: `${endpoint}${context.url}`, + init: context.init ?? {} + } + }, + post: async (context: ResponseContext) => { + const response = context.response as Response + const { currentVersion, unhealthyBlockDiff, unhealthySlotDiffPlays } = + discoveryProviderSelector + const endpoint = await selectionPromise + if (!endpoint) { + throw new Error( + 'All Discovery Providers are unhealthy and unavailable.' + ) + } + if (response.ok) { + // Even when successful, copy response to read JSON body and check for signs the DN is unhealthy + // Prevents stale data + const responseClone = response.clone() + const json = await responseClone.json() + if ( + !isApiResponseHealthy({ + data: json, + endpoint, + currentVersion, + unhealthyBlockDiff, + unhealthySlotDiffPlays + }) + ) { + const { newSelectionPromise, response } = await reselectAndRetry({ + endpoint, + discoveryProviderSelector, + context + }) + selectionPromise = newSelectionPromise + return response + } + } else { + // On request failure, check health_check and reselect if unhealthy + console.warn('Audius SDK request failed:', context) + const isHealthy = await isDiscoveryNodeHealthy({ + endpoint, + currentVersion, + unhealthyBlockDiff, + unhealthySlotDiffPlays + }) + if (!isHealthy) { + const { newSelectionPromise, response } = await reselectAndRetry({ + endpoint, + discoveryProviderSelector, + context + }) + selectionPromise = newSelectionPromise + return response + } + } + return response + } + } +} diff --git a/libs/src/sdk/middleware/index.ts b/libs/src/sdk/middleware/index.ts new file mode 100644 index 00000000000..e44306a491b --- /dev/null +++ b/libs/src/sdk/middleware/index.ts @@ -0,0 +1,3 @@ +export { discoveryNodeSelectorMiddleware } from './discoveryNodeSelectorMiddleware' +export { addAppNameMiddleware } from './addAppNameMiddleware' +export { jsonResponseMiddleware } from './jsonResponseMiddleware' diff --git a/libs/src/sdk/middleware/jsonResponseMiddleware.ts b/libs/src/sdk/middleware/jsonResponseMiddleware.ts new file mode 100644 index 00000000000..21b15ed9db8 --- /dev/null +++ b/libs/src/sdk/middleware/jsonResponseMiddleware.ts @@ -0,0 +1,23 @@ +import type { ResponseContext } from '../api/generated/default' + +/** + * Parses the JSON response body and returns it, replacing the return value of the Response with an Object + * Note: Place this last in the list of middlewares to avoid conflicts with other middleware that depends on the Response + * @param options options for the middleware + * @param {boolean} options.extractData- whether or not to get the nested `data` property + */ +export const jsonResponseMiddleware = ({ + extractData = false +}: { + extractData: boolean +}) => { + return { + post: async (context: ResponseContext) => { + const json = await (context.response as Response).json() + if (extractData) { + return json.data + } + return json + } + } +} diff --git a/libs/src/sdk/sdk.ts b/libs/src/sdk/sdk.ts index 9db34c33f65..55fb588854a 100644 --- a/libs/src/sdk/sdk.ts +++ b/libs/src/sdk/sdk.ts @@ -9,12 +9,14 @@ import { UserStateManager } from '../userStateManager' import { Oauth } from './oauth' import { TracksApi } from './api/TracksApi' import { ResolveApi } from './api/ResolveApi' +import { ChatsApi } from './api/chats/ChatsApi' import { Configuration, PlaylistsApi, UsersApi, TipsApi, - querystring + WalletAPI, + RequiredError } from './api/generated/default' import { Configuration as ConfigurationFull, @@ -26,6 +28,7 @@ import { TipsApi as TipsApiFull, TransactionsApi as TransactionsApiFull } from './api/generated/full' +import fetch from 'cross-fetch' import { CLAIM_DISTRIBUTION_CONTRACT_ADDRESS, @@ -38,6 +41,11 @@ import { } from './constants' import { getPlatformLocalStorage, LocalStorage } from '../utils/localStorage' import type { SetOptional } from 'type-fest' +import { + addAppNameMiddleware, + jsonResponseMiddleware, + discoveryNodeSelectorMiddleware +} from './middleware' type Web3Config = { providers: string[] @@ -75,19 +83,43 @@ type SdkConfig = { * Configuration for Web3 */ web3Config?: Web3Config + /** + * Helpers to faciliate requests that require signatures or encryption + */ + walletApi?: WalletAPI +} + +/** + * Default wallet API which is used to surface errors when the walletApi is not configured + */ +const defaultWalletAPI: WalletAPI = { + getSharedSecret: async (_: string | Uint8Array): Promise => { + throw new RequiredError( + 'Wallet API configuration missing. This method requires using the walletApi config for write access.' + ) + }, + sign: async (_: string): Promise<[Uint8Array, number]> => { + throw new RequiredError( + 'Wallet API configuration missing. This method requires using the walletApi config for write access.' + ) + } } /** * The Audius SDK */ export const sdk = (config: SdkConfig) => { - const { appName } = config + const { appName, walletApi } = config // Initialize services const { discoveryProvider } = initializeServices(config) // Initialize APIs - const apis = initializeApis({ appName, discoveryProvider }) + const apis = initializeApis({ + appName, + discoveryProvider, + walletApi: walletApi ?? defaultWalletAPI + }) // Initialize OAuth const oauth = @@ -147,35 +179,26 @@ const initializeServices = (config: SdkConfig) => { const initializeApis = ({ appName, - discoveryProvider + discoveryProvider, + walletApi }: { appName: string discoveryProvider: DiscoveryProvider + walletApi: WalletAPI }) => { - const initializationPromise = discoveryProvider.init() - - const fetchApi = async (url: string, context?: RequestInit) => { - // Ensure discovery node is initialized - await initializationPromise - - // Append the appName to the query params - const urlWithAppName = - url + (url.includes('?') ? '&' : '?') + querystring({ app_name: appName }) - const requestParams: Record = { - ...context, - endpoint: urlWithAppName - } - return await discoveryProvider._makeRequest( - requestParams, - undefined, - undefined, - // Throw errors instead of returning null - true - ) - } - + const defaultMiddleware = [ + addAppNameMiddleware({ appName }), + discoveryNodeSelectorMiddleware({ + discoveryProviderSelector: discoveryProvider.serviceSelector + }) + ] const generatedApiClientConfig = new Configuration({ - fetchApi + fetchApi: fetch, + middleware: [ + ...defaultMiddleware, + jsonResponseMiddleware({ extractData: true }) + ], + walletApi }) const tracks = new TracksApi(generatedApiClientConfig, discoveryProvider) @@ -183,9 +206,25 @@ const initializeApis = ({ const playlists = new PlaylistsApi(generatedApiClientConfig) const tips = new TipsApi(generatedApiClientConfig) const { resolve } = new ResolveApi(generatedApiClientConfig) + const chats = new ChatsApi( + new Configuration({ + fetchApi: fetch, + walletApi, + basePath: '', + middleware: [ + ...defaultMiddleware, + jsonResponseMiddleware({ extractData: false }) + ] + }) + ) const generatedApiClientConfigFull = new ConfigurationFull({ - fetchApi + fetchApi: fetch, + middleware: [ + ...defaultMiddleware, + jsonResponseMiddleware({ extractData: true }) + ], + walletApi }) const full = { @@ -204,7 +243,8 @@ const initializeApis = ({ playlists, tips, resolve, - full + full, + chats } } diff --git a/libs/tsconfig.json b/libs/tsconfig.json index ad4341fa81b..4f715bf4102 100644 --- a/libs/tsconfig.json +++ b/libs/tsconfig.json @@ -5,6 +5,7 @@ "checkJs": false, "allowJs": true, "resolveJsonModule": true, + "moduleResolution": "node", "typeRoots": ["./types", "./node_modules/@types"], "exactOptionalPropertyTypes": false, "declaration": true, @@ -12,5 +13,5 @@ "declarationDir": "./" }, "exclude": ["dist"], - "include": ["src/**/*",] + "include": ["src/**/*"] }