From 95306a7cd062dda0c6b35a2ebea0f1342bd65842 Mon Sep 17 00:00:00 2001 From: AJtheManager Date: Mon, 29 Jun 2026 11:07:35 +0100 Subject: [PATCH] feat(stellar): SEP-0002 federation address resolver resolveStellarFederation() lets stealth payment senders accept name*domain.tld addresses by fetching .well-known/stellar.toml, querying FEDERATION_SERVER, validating the response, and caching the result. HTTPS-only by default, configurable TTL, injectable fetch for tests. --- pnpm-lock.yaml | 771 ++++++++++++++++++++++++- src/chains/stellar/federation.ts | 370 ++++++++++++ src/chains/stellar/index.ts | 16 + test/chains/stellar/federation.test.ts | 346 +++++++++++ 4 files changed, 1483 insertions(+), 20 deletions(-) create mode 100644 src/chains/stellar/federation.ts create mode 100644 test/chains/stellar/federation.test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index deda819..2db757a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,13 +47,26 @@ importers: version: 2.9.0 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.15)(typescript@5.9.3)(yaml@2.9.0) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) typescript: specifier: ^5.7.0 version: 5.9.3 vitest: specifier: ^3.1.0 - version: 3.2.6(@types/node@26.0.0)(jiti@2.6.1)(jsdom@24.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(terser@5.48.0)(yaml@2.9.0) + version: 3.2.6(@types/node@26.0.0)(jiti@2.6.1)(jsdom@24.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + + examples/multichain-scan: + dependencies: + '@wraith-protocol/sdk': + specifier: file:../.. + version: file:(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@stellar/stellar-sdk@13.3.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + devDependencies: + tsx: + specifier: ^4.0.0 + version: 4.22.4 + typescript: + specifier: ^5.7.0 + version: 5.9.3 examples/react-native-stellar: dependencies: @@ -120,6 +133,73 @@ importers: specifier: ^5.4.1 version: 5.4.21(@types/node@26.0.0)(terser@5.48.0) + examples/stellar-cli-scan: + dependencies: + '@wraith-protocol/sdk': + specifier: file:../.. + version: file:(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@stellar/stellar-sdk@13.3.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + devDependencies: + tsx: + specifier: ^4.0.0 + version: 4.22.4 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + + examples/stellar-cli-send: + dependencies: + '@wraith-protocol/sdk': + specifier: file:../.. + version: file:(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@stellar/stellar-sdk@13.3.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + devDependencies: + tsx: + specifier: ^4.0.0 + version: 4.22.4 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + + examples/stellar-react-receive: + dependencies: + '@wraith-protocol/sdk': + specifier: file:../.. + version: file:(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@stellar/stellar-sdk@13.3.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + react: + specifier: ^19.0.0 + version: 19.2.7 + react-dom: + specifier: ^19.0.0 + version: 19.2.7(react@19.2.7) + devDependencies: + '@types/react': + specifier: ^19.0.0 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^4.4.0 + version: 4.7.0(vite@6.4.3(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vite: + specifier: ^6.1.0 + version: 6.4.3(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + + examples/stellar-spectre-agent: + dependencies: + '@wraith-protocol/sdk': + specifier: file:../.. + version: file:(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@stellar/stellar-sdk@13.3.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + devDependencies: + tsx: + specifier: ^4.0.0 + version: 4.22.4 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + packages/sdk-react: dependencies: '@stellar/stellar-sdk': @@ -152,13 +232,13 @@ importers: version: 24.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) tsup: specifier: ^8.0.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.15)(typescript@5.9.3)(yaml@2.9.0) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) typescript: specifier: ^5.0.0 version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.6(@types/node@26.0.0)(jiti@2.6.1)(jsdom@24.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(terser@5.48.0)(yaml@2.9.0) + version: 3.2.6(@types/node@26.0.0)(jiti@2.6.1)(jsdom@24.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) packages: @@ -1010,297 +1090,609 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.7': resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.7': resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.7': resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.7': resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.7': resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.7': resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.7': resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.7': resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.7': resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.7': resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.7': resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.7': resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.7': resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.7': resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.7': resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.7': resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.7': resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.7': resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.18.31': resolution: {integrity: sha512-v9llw9fT3Uv+TCM6Xllo54t672CuYtinEQZ2LPJ2EJsCwuTc4Cd2gXQaouuIVD21VoeGQnr5JtJuWbF97sBKzQ==} @@ -1862,9 +2254,17 @@ packages: peerDependencies: '@types/react': ^18.0.0 + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + '@types/react@18.3.31': resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -2818,11 +3218,21 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4566,6 +4976,11 @@ packages: peerDependencies: react: ^18.3.1 + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4608,6 +5023,10 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -4785,6 +5204,9 @@ packages: scheduler@0.24.0-canary-efb381bbf-20230505: resolution: {integrity: sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + selfsigned@2.4.1: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} @@ -5244,6 +5666,11 @@ packages: typescript: optional: true + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} @@ -5474,6 +5901,46 @@ packages: terser: optional: true + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@7.3.5: resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6856,150 +7323,306 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm64@0.28.1': + optional: true + '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-arm@0.28.1': + optional: true + '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/android-x64@0.28.1': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.28.1': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/darwin-x64@0.28.1': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.28.1': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.28.1': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm64@0.28.1': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-arm@0.28.1': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-ia32@0.28.1': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-loong64@0.28.1': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-mips64el@0.28.1': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-ppc64@0.28.1': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.28.1': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-s390x@0.28.1': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.28.1': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.28.1': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.28.1': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.27.7': optional: true + '@esbuild/win32-x64@0.28.1': + optional: true + '@expo/bunyan@4.0.1': dependencies: uuid: 8.3.2 @@ -7988,11 +8611,19 @@ snapshots: dependencies: '@types/react': 18.3.31 + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + '@types/react@18.3.31': dependencies: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + '@types/stack-utils@2.0.3': {} '@types/uuid@10.0.0': {} @@ -8047,6 +8678,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.3(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.6': dependencies: '@types/chai': 5.2.3 @@ -8055,13 +8698,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0))': + '@vitest/mocker@3.2.6(vite@7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0) + vite: 7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) '@vitest/pretty-format@3.2.6': dependencies: @@ -9095,6 +9738,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -9124,6 +9796,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -10944,12 +11645,13 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.15)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.15 + tsx: 4.22.4 yaml: 2.9.0 postcss@8.4.49: @@ -11071,6 +11773,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -11145,6 +11852,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + react@19.2.7: {} + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -11375,6 +12084,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + scheduler@0.27.0: {} + selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.14 @@ -11843,7 +12554,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.15)(typescript@5.9.3)(yaml@2.9.0): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -11854,7 +12565,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.15)(yaml@2.9.0) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0) resolve-from: 5.0.0 rollup: 4.62.2 source-map: 0.7.6 @@ -11871,6 +12582,12 @@ snapshots: - tsx - yaml + tsx@4.22.4: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + tweetnacl@1.0.3: {} type-detect@4.0.8: {} @@ -12049,16 +12766,15 @@ snapshots: - utf-8-validate - zod - vite-node@3.2.4(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0): + vite-node@3.2.4(@types/node@26.0.0)(terser@5.48.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0) + vite: 5.4.21(@types/node@26.0.0)(terser@5.48.0) transitivePeerDependencies: - '@types/node' - - jiti - less - lightningcss - sass @@ -12067,8 +12783,6 @@ snapshots: - sugarss - supports-color - terser - - tsx - - yaml vite@5.4.21(@types/node@26.0.0)(terser@5.48.0): dependencies: @@ -12080,7 +12794,23 @@ snapshots: fsevents: 2.3.3 terser: 5.48.0 - vite@7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0): + vite@6.4.3(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.0.0 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.48.0 + tsx: 4.22.4 + yaml: 2.9.0 + + vite@7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -12093,13 +12823,14 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 terser: 5.48.0 + tsx: 4.22.4 yaml: 2.9.0 - vitest@3.2.6(@types/node@26.0.0)(jiti@2.6.1)(jsdom@24.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(terser@5.48.0)(yaml@2.9.0): + vitest@3.2.6(@types/node@26.0.0)(jiti@2.6.1)(jsdom@24.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0)) + '@vitest/mocker': 3.2.6(vite@7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.6 '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -12117,8 +12848,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0) + vite: 7.3.5(@types/node@26.0.0)(jiti@2.6.1)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@26.0.0)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.0.0 diff --git a/src/chains/stellar/federation.ts b/src/chains/stellar/federation.ts new file mode 100644 index 0000000..a6cabc2 --- /dev/null +++ b/src/chains/stellar/federation.ts @@ -0,0 +1,370 @@ +import { StrKey } from '@stellar/stellar-sdk'; + +/** + * Default cache TTL (1 hour) for resolved federation addresses. + * + * @see {@link setFederationDefaultTtl} + */ +export const DEFAULT_FEDERATION_TTL_MS = 60 * 60 * 1000; + +/** Default per-request timeout in milliseconds. */ +const DEFAULT_TIMEOUT_MS = 10_000; + +/** SEP-0001 well-known stellar.toml path. */ +const STELLAR_TOML_PATH = '/.well-known/stellar.toml'; + +/** + * Validates the structural form of a federation address (`name*domain.tld`). + * + * The Stellar Federation spec (SEP-0002) allows alphanumerics, dots, dashes, + * underscores, and `+` in the `name`; the domain must contain at least one dot. + */ +const FEDERATION_REGEX = /^[A-Za-z0-9._+\-]+\*[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$/; + +/** Supported Stellar memo types as per SEP-0002. */ +export type FederationMemoType = 'text' | 'id' | 'hash'; + +/** Memo specified by a federation server for a resolved address. */ +export interface FederationMemo { + /** Memo type — one of `text`, `id`, or `hash`. */ + type: FederationMemoType; + /** Memo value (text payload, integer string, or hex hash). */ + value: string; +} + +/** Typed result returned by {@link resolveStellarFederation}. */ +export interface FederationResolution { + /** Stellar account ID (G...) the federation address resolves to. */ + accountId: string; + /** The federation address as reported by the server (or the original input). */ + stellarAddress: string; + /** Optional memo specified by the federation server. */ + memo?: FederationMemo; + /** Unix milliseconds when the resolution was produced. */ + resolvedAt: number; +} + +/** Discriminator for {@link FederationResolutionError} consumers. */ +export type FederationErrorCode = + | 'INVALID_ADDRESS' + | 'TOML_FETCH_FAILED' + | 'FEDERATION_SERVER_MISSING' + | 'FEDERATION_SERVER_FAILED' + | 'INVALID_RESPONSE' + | 'TIMEOUT' + | 'INSECURE_PROTOCOL'; + +/** + * Decoded error thrown by {@link resolveStellarFederation} whenever a lookup + * cannot be completed. The `code` field categorises the failure for + * programmatic handling; the `message` is a human-readable explanation. + */ +export class FederationResolutionError extends Error { + readonly code: FederationErrorCode; + + constructor(code: FederationErrorCode, message: string, cause?: unknown) { + super(message); + this.name = 'FederationResolutionError'; + this.code = code; + if (cause !== undefined) { + (this as { cause?: unknown }).cause = cause; + } + } +} + +/** Per-call configuration for {@link resolveStellarFederation}. */ +export interface ResolveFederationOptions { + /** + * Cache TTL in milliseconds for this resolution. Defaults to the value set + * by {@link setFederationDefaultTtl} (1 hour at module load). Use `0` to + * skip writing this entry to the cache. + */ + cacheTtlMs?: number; + /** Bypass any cached entry for this call (and skip writing the result). */ + noCache?: boolean; + /** Custom fetch implementation — useful for tests, proxies, or RN polyfills. */ + fetchImpl?: typeof fetch; + /** Per-request timeout in milliseconds. Default `10_000`. */ + timeoutMs?: number; + /** + * Allow http:// URLs. SEP-0001 requires HTTPS, so by default any + * `FEDERATION_SERVER` that uses http will be rejected. Only set this to + * `true` for testing against local servers. + */ + allowInsecureHttp?: boolean; +} + +interface CacheEntry { + expiresAt: number; + resolution: FederationResolution; +} + +const cache = new Map(); +let defaultTtlMs = DEFAULT_FEDERATION_TTL_MS; + +/** + * Resolve a Stellar federation address (SEP-0002, `name*domain.com`) into a + * Stellar account ID and optional memo. + * + * Resolution flow: + * 1. Validates the input shape. Already-encoded `G...` and `M...` account IDs + * are passed through unchanged. + * 2. Fetches `https:///.well-known/stellar.toml` (SEP-0001) and + * extracts `FEDERATION_SERVER`. + * 3. Queries `?q=
&type=name`. + * 4. Validates the response (account_id strkey, optional memo) and caches it. + * + * Results are cached in-memory by address for the configured TTL (default + * 1 hour). Call {@link clearFederationCache} to drop cached entries or + * {@link setFederationDefaultTtl} to change the default. + * + * @param input Federation address (`name*domain.com`) or a Stellar account + * ID (`G...` or `M...`), which is returned without a network + * round-trip. + * @param options Per-call cache / fetch / timeout overrides. + * @throws {FederationResolutionError} If lookup fails at any step. + * + * @example + * ```ts + * import { resolveStellarFederation } from '@wraith-protocol/sdk/chains/stellar'; + * + * const { accountId, memo } = await resolveStellarFederation('alice*example.com'); + * if (memo) console.log(`Send with memo ${memo.type}=${memo.value}`); + * ``` + */ +export async function resolveStellarFederation( + input: string, + options: ResolveFederationOptions = {}, +): Promise { + const address = typeof input === 'string' ? input.trim() : ''; + if (!address) { + throw new FederationResolutionError('INVALID_ADDRESS', 'federation address is empty'); + } + + if (isStellarAccountId(address)) { + return { + accountId: address, + stellarAddress: address, + resolvedAt: Date.now(), + }; + } + + if (!FEDERATION_REGEX.test(address)) { + throw new FederationResolutionError( + 'INVALID_ADDRESS', + `not a valid federation address: "${address}". Expected "name*domain.tld".`, + ); + } + + const noCache = options.noCache === true; + if (!noCache) { + const cached = readCache(address); + if (cached) return cached; + } + + const domain = address.split('*')[1]; + const resolution = await performResolution(address, domain, options); + + if (!noCache) { + const ttl = options.cacheTtlMs ?? defaultTtlMs; + if (ttl > 0) { + cache.set(address, { resolution, expiresAt: Date.now() + ttl }); + } + } + + return resolution; +} + +/** + * Clear cached federation resolutions. + * + * @param address Optional federation address to evict. When omitted, the + * entire cache is cleared. + */ +export function clearFederationCache(address?: string): void { + if (address) cache.delete(address); + else cache.clear(); +} + +/** + * Change the default TTL applied to newly cached federation resolutions. + * Existing entries keep their original expiry. + * + * @param ttlMs A non-negative finite number of milliseconds. `0` disables + * caching for subsequent resolutions that don't override the TTL. + */ +export function setFederationDefaultTtl(ttlMs: number): void { + if (!Number.isFinite(ttlMs) || ttlMs < 0) { + throw new Error('ttlMs must be a non-negative finite number'); + } + defaultTtlMs = ttlMs; +} + +/** Returns the current default cache TTL in milliseconds. */ +export function getFederationDefaultTtl(): number { + return defaultTtlMs; +} + +async function performResolution( + address: string, + domain: string, + options: ResolveFederationOptions, +): Promise { + const tomlUrl = `https://${domain}${STELLAR_TOML_PATH}`; + const tomlText = await fetchText(tomlUrl, options, 'TOML_FETCH_FAILED'); + const federationServer = parseFederationServer(tomlText); + + if (!federationServer) { + throw new FederationResolutionError( + 'FEDERATION_SERVER_MISSING', + `${domain} stellar.toml does not define FEDERATION_SERVER`, + ); + } + if (!/^https?:\/\//i.test(federationServer)) { + throw new FederationResolutionError( + 'INVALID_RESPONSE', + `FEDERATION_SERVER is not a valid URL: ${federationServer}`, + ); + } + if (/^http:\/\//i.test(federationServer) && options.allowInsecureHttp !== true) { + throw new FederationResolutionError( + 'INSECURE_PROTOCOL', + `FEDERATION_SERVER must use HTTPS: ${federationServer}`, + ); + } + + const sep = federationServer.includes('?') ? '&' : '?'; + const queryUrl = `${federationServer}${sep}q=${encodeURIComponent(address)}&type=name`; + const text = await fetchText(queryUrl, options, 'FEDERATION_SERVER_FAILED'); + + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch (err) { + throw new FederationResolutionError( + 'INVALID_RESPONSE', + `federation server response is not JSON: ${text.slice(0, 120)}`, + err, + ); + } + + return shapeResponse(parsed, address); +} + +function shapeResponse(parsed: unknown, address: string): FederationResolution { + if (!parsed || typeof parsed !== 'object') { + throw new FederationResolutionError( + 'INVALID_RESPONSE', + 'federation response is not a JSON object', + ); + } + const obj = parsed as Record; + + if (typeof obj.detail === 'string' && typeof obj.account_id !== 'string') { + throw new FederationResolutionError( + 'INVALID_RESPONSE', + `federation server error: ${obj.detail}`, + ); + } + + const accountId = obj.account_id; + if (typeof accountId !== 'string' || !isStellarAccountId(accountId)) { + throw new FederationResolutionError( + 'INVALID_RESPONSE', + `federation response missing or invalid account_id: ${String(accountId)}`, + ); + } + + let memo: FederationMemo | undefined; + if (obj.memo_type !== undefined || obj.memo !== undefined) { + const type = obj.memo_type; + const value = obj.memo; + if (typeof type !== 'string' || (value !== undefined && typeof value !== 'string')) { + throw new FederationResolutionError( + 'INVALID_RESPONSE', + 'memo_type must be a string and memo must be a string when provided', + ); + } + if (type !== 'text' && type !== 'id' && type !== 'hash') { + throw new FederationResolutionError( + 'INVALID_RESPONSE', + `unsupported memo_type "${type}". Expected "text", "id", or "hash".`, + ); + } + if (typeof value === 'string') { + memo = { type, value }; + } + } + + const reported = typeof obj.stellar_address === 'string' ? obj.stellar_address : address; + return { accountId, stellarAddress: reported, memo, resolvedAt: Date.now() }; +} + +async function fetchText( + url: string, + options: ResolveFederationOptions, + failureCode: FederationErrorCode, +): Promise { + const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const fetchImpl = options.fetchImpl ?? globalThis.fetch; + if (typeof fetchImpl !== 'function') { + throw new FederationResolutionError( + failureCode, + 'no global fetch available — pass options.fetchImpl', + ); + } + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetchImpl(url, { + headers: { Accept: 'application/json, text/plain, */*' }, + signal: controller.signal, + }); + if (!res.ok) { + throw new FederationResolutionError( + failureCode, + `request failed: ${res.status} ${res.statusText || ''} (${url})`.trim(), + ); + } + return await res.text(); + } catch (err) { + if (err instanceof FederationResolutionError) throw err; + const name = (err as { name?: string } | null)?.name; + if (name === 'AbortError' || name === 'TimeoutError') { + throw new FederationResolutionError( + 'TIMEOUT', + `request timed out after ${timeoutMs} ms (${url})`, + err, + ); + } + const message = err instanceof Error ? err.message : String(err); + throw new FederationResolutionError(failureCode, `request error for ${url}: ${message}`, err); + } finally { + clearTimeout(timer); + } +} + +function parseFederationServer(toml: string): string | null { + const match = toml.match(/^\s*FEDERATION_SERVER\s*=\s*(?:"([^"]+)"|'([^']+)')/m); + if (!match) return null; + return match[1] ?? match[2] ?? null; +} + +function readCache(address: string): FederationResolution | null { + const entry = cache.get(address); + if (!entry) return null; + if (entry.expiresAt <= Date.now()) { + cache.delete(address); + return null; + } + return entry.resolution; +} + +function isStellarAccountId(value: string): boolean { + if (StrKey.isValidEd25519PublicKey(value)) return true; + const muxed = (StrKey as unknown as { isValidMed25519PublicKey?: (v: string) => boolean }) + .isValidMed25519PublicKey; + if (typeof muxed === 'function' && muxed(value)) return true; + return false; +} diff --git a/src/chains/stellar/index.ts b/src/chains/stellar/index.ts index a8375a9..65d7efd 100644 --- a/src/chains/stellar/index.ts +++ b/src/chains/stellar/index.ts @@ -82,3 +82,19 @@ export type { export { buildStellarSwapAndStealth } from './swap'; export type { BuildStellarSwapAndStealthOptions, SwapAndStealthResult } from './swap'; + +export { + resolveStellarFederation, + clearFederationCache, + setFederationDefaultTtl, + getFederationDefaultTtl, + FederationResolutionError, + DEFAULT_FEDERATION_TTL_MS, +} from './federation'; +export type { + FederationResolution, + FederationMemo, + FederationMemoType, + FederationErrorCode, + ResolveFederationOptions, +} from './federation'; diff --git a/test/chains/stellar/federation.test.ts b/test/chains/stellar/federation.test.ts new file mode 100644 index 0000000..c53dd99 --- /dev/null +++ b/test/chains/stellar/federation.test.ts @@ -0,0 +1,346 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Keypair } from '@stellar/stellar-sdk'; +import { + FederationResolutionError, + clearFederationCache, + getFederationDefaultTtl, + resolveStellarFederation, + setFederationDefaultTtl, +} from '../../../src/chains/stellar/federation'; + +type FetchInput = { url: string; init?: RequestInit }; + +function textResponse(body: string, init: { ok?: boolean; status?: number } = {}): Response { + return { + ok: init.ok ?? true, + status: init.status ?? 200, + statusText: '', + text: async () => body, + } as unknown as Response; +} + +function jsonResponse(body: unknown, init: { ok?: boolean; status?: number } = {}): Response { + return textResponse(JSON.stringify(body), init); +} + +function mockFetch(handler: (call: FetchInput) => Response | Promise) { + const calls: FetchInput[] = []; + const impl = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === 'string' ? input : (input as URL).toString(); + const call = { url, init }; + calls.push(call); + return handler(call); + }); + return { impl, calls }; +} + +const tomlBody = (federationUrl: string) => ` +NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" +FEDERATION_SERVER = "${federationUrl}" +WEB_AUTH_ENDPOINT = "https://example.com/auth" +`; + +describe('resolveStellarFederation', () => { + const recipient = Keypair.random(); + const federationUrl = 'https://federation.example.com/federation'; + + beforeEach(() => { + clearFederationCache(); + setFederationDefaultTtl(60 * 60 * 1000); + }); + + afterEach(() => { + vi.restoreAllMocks(); + clearFederationCache(); + }); + + it('resolves a federation address through stellar.toml + federation server', async () => { + const { impl, calls } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) { + return textResponse(tomlBody(federationUrl)); + } + if (url.startsWith(federationUrl)) { + return jsonResponse({ + stellar_address: 'alice*example.com', + account_id: recipient.publicKey(), + memo_type: 'text', + memo: 'invoice-42', + }); + } + throw new Error(`unexpected url ${url}`); + }); + + const result = await resolveStellarFederation('alice*example.com', { fetchImpl: impl }); + + expect(result.accountId).toBe(recipient.publicKey()); + expect(result.memo).toEqual({ type: 'text', value: 'invoice-42' }); + expect(result.stellarAddress).toBe('alice*example.com'); + expect(result.resolvedAt).toBeTypeOf('number'); + + expect(calls).toHaveLength(2); + expect(calls[0].url).toBe('https://example.com/.well-known/stellar.toml'); + expect(calls[1].url).toBe( + `${federationUrl}?q=${encodeURIComponent('alice*example.com')}&type=name`, + ); + }); + + it('returns a result without memo when none is provided', async () => { + const { impl } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ + stellar_address: 'bob*example.com', + account_id: recipient.publicKey(), + }); + }); + + const result = await resolveStellarFederation('bob*example.com', { fetchImpl: impl }); + + expect(result.accountId).toBe(recipient.publicKey()); + expect(result.memo).toBeUndefined(); + }); + + it('caches resolutions for the configured TTL', async () => { + const { impl, calls } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ account_id: recipient.publicKey() }); + }); + + await resolveStellarFederation('carol*example.com', { fetchImpl: impl }); + await resolveStellarFederation('carol*example.com', { fetchImpl: impl }); + await resolveStellarFederation('carol*example.com', { fetchImpl: impl }); + + expect(calls).toHaveLength(2); + }); + + it('skips the cache when noCache is set', async () => { + const { impl, calls } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ account_id: recipient.publicKey() }); + }); + + await resolveStellarFederation('dan*example.com', { fetchImpl: impl }); + await resolveStellarFederation('dan*example.com', { fetchImpl: impl, noCache: true }); + await resolveStellarFederation('dan*example.com', { fetchImpl: impl, noCache: true }); + + expect(calls).toHaveLength(6); + }); + + it('does not cache when cacheTtlMs is 0', async () => { + const { impl, calls } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ account_id: recipient.publicKey() }); + }); + + await resolveStellarFederation('eve*example.com', { fetchImpl: impl, cacheTtlMs: 0 }); + await resolveStellarFederation('eve*example.com', { fetchImpl: impl, cacheTtlMs: 0 }); + + expect(calls).toHaveLength(4); + }); + + it('refetches once an entry has expired', async () => { + setFederationDefaultTtl(50); + const { impl, calls } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ account_id: recipient.publicKey() }); + }); + + await resolveStellarFederation('frank*example.com', { fetchImpl: impl }); + await new Promise((resolve) => setTimeout(resolve, 80)); + await resolveStellarFederation('frank*example.com', { fetchImpl: impl }); + + expect(calls).toHaveLength(4); + }); + + it('passes through a valid G... account ID without hitting the network', async () => { + const { impl, calls } = mockFetch(() => textResponse('should not be called', { ok: false })); + + const result = await resolveStellarFederation(recipient.publicKey(), { fetchImpl: impl }); + + expect(result.accountId).toBe(recipient.publicKey()); + expect(result.stellarAddress).toBe(recipient.publicKey()); + expect(result.memo).toBeUndefined(); + expect(calls).toHaveLength(0); + }); + + it('rejects malformed federation addresses with INVALID_ADDRESS', async () => { + await expect( + resolveStellarFederation('not-a-federation-address', { fetchImpl: vi.fn() }), + ).rejects.toMatchObject({ + name: 'FederationResolutionError', + code: 'INVALID_ADDRESS', + }); + + await expect( + resolveStellarFederation('alice@example.com', { fetchImpl: vi.fn() }), + ).rejects.toMatchObject({ code: 'INVALID_ADDRESS' }); + + await expect( + resolveStellarFederation('alice*nodot', { fetchImpl: vi.fn() }), + ).rejects.toMatchObject({ code: 'INVALID_ADDRESS' }); + + await expect(resolveStellarFederation('', { fetchImpl: vi.fn() })).rejects.toMatchObject({ + code: 'INVALID_ADDRESS', + }); + }); + + it('throws TOML_FETCH_FAILED on stellar.toml HTTP errors', async () => { + const { impl } = mockFetch(() => textResponse('not found', { ok: false, status: 404 })); + + await expect( + resolveStellarFederation('gina*example.com', { fetchImpl: impl }), + ).rejects.toMatchObject({ code: 'TOML_FETCH_FAILED' }); + }); + + it('throws FEDERATION_SERVER_MISSING when stellar.toml has no FEDERATION_SERVER', async () => { + const { impl } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) { + return textResponse('NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"'); + } + throw new Error('unreachable'); + }); + + await expect( + resolveStellarFederation('harry*example.com', { fetchImpl: impl }), + ).rejects.toMatchObject({ code: 'FEDERATION_SERVER_MISSING' }); + }); + + it('rejects http:// federation servers unless allowInsecureHttp is set', async () => { + const tomlInsecure = tomlBody('http://insecure.example.com/federation'); + const { impl } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlInsecure); + return jsonResponse({ account_id: recipient.publicKey() }); + }); + + await expect( + resolveStellarFederation('isaac*example.com', { fetchImpl: impl }), + ).rejects.toMatchObject({ code: 'INSECURE_PROTOCOL' }); + + clearFederationCache(); + const allowed = await resolveStellarFederation('isaac*example.com', { + fetchImpl: impl, + allowInsecureHttp: true, + }); + expect(allowed.accountId).toBe(recipient.publicKey()); + }); + + it('throws FEDERATION_SERVER_FAILED on federation server HTTP errors', async () => { + const { impl } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return textResponse('forbidden', { ok: false, status: 403 }); + }); + + await expect( + resolveStellarFederation('jess*example.com', { fetchImpl: impl }), + ).rejects.toMatchObject({ code: 'FEDERATION_SERVER_FAILED' }); + }); + + it('throws INVALID_RESPONSE when account_id is missing', async () => { + const { impl } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ stellar_address: 'kev*example.com' }); + }); + + await expect( + resolveStellarFederation('kev*example.com', { fetchImpl: impl }), + ).rejects.toMatchObject({ code: 'INVALID_RESPONSE' }); + }); + + it('throws INVALID_RESPONSE when account_id is not a Stellar StrKey', async () => { + const { impl } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ account_id: 'not-a-strkey' }); + }); + + await expect( + resolveStellarFederation('luke*example.com', { fetchImpl: impl }), + ).rejects.toMatchObject({ code: 'INVALID_RESPONSE' }); + }); + + it('surfaces federation server error bodies via INVALID_RESPONSE', async () => { + const { impl } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ detail: 'unknown user' }); + }); + + await expect( + resolveStellarFederation('mia*example.com', { fetchImpl: impl }), + ).rejects.toMatchObject({ + code: 'INVALID_RESPONSE', + message: expect.stringMatching(/unknown user/), + }); + }); + + it('throws INVALID_RESPONSE on an unsupported memo_type', async () => { + const { impl } = mockFetch(({ url }) => { + if (url.endsWith('/.well-known/stellar.toml')) return textResponse(tomlBody(federationUrl)); + return jsonResponse({ + account_id: recipient.publicKey(), + memo_type: 'return', + memo: '12345', + }); + }); + + await expect( + resolveStellarFederation('nora*example.com', { fetchImpl: impl }), + ).rejects.toMatchObject({ code: 'INVALID_RESPONSE' }); + }); + + it('honors the per-request timeout', async () => { + const impl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { + return await new Promise((_, reject) => { + init?.signal?.addEventListener('abort', () => { + const err = new Error('aborted'); + err.name = 'AbortError'; + reject(err); + }); + }); + }); + + await expect( + resolveStellarFederation('owen*example.com', { fetchImpl: impl, timeoutMs: 15 }), + ).rejects.toMatchObject({ code: 'TIMEOUT' }); + }); + + it('exposes setFederationDefaultTtl / getFederationDefaultTtl', () => { + setFederationDefaultTtl(123); + expect(getFederationDefaultTtl()).toBe(123); + expect(() => setFederationDefaultTtl(-1)).toThrow(); + expect(() => setFederationDefaultTtl(Number.NaN)).toThrow(); + }); + + it('FederationResolutionError exposes code + cause', () => { + const inner = new Error('boom'); + const err = new FederationResolutionError('TIMEOUT', 'fetch timed out', inner); + expect(err.name).toBe('FederationResolutionError'); + expect(err.code).toBe('TIMEOUT'); + expect((err as { cause?: unknown }).cause).toBe(inner); + }); +}); + +// --------------------------------------------------------------------------- +// Opt-in integration test against a real federation server. +// Set FEDERATION_INTEGRATION=1 and FEDERATION_ADDRESS= to run. +// --------------------------------------------------------------------------- + +const SKIP_INTEGRATION = process.env['FEDERATION_INTEGRATION'] !== '1'; + +describe('resolveStellarFederation integration', { skip: SKIP_INTEGRATION }, () => { + it('resolves a configured live federation address', async () => { + const address = process.env['FEDERATION_ADDRESS']; + if (!address) throw new Error('FEDERATION_ADDRESS is required when FEDERATION_INTEGRATION=1'); + + clearFederationCache(); + const result = await resolveStellarFederation(address, { timeoutMs: 15_000 }); + + expect(result.accountId).toMatch(/^G[A-Z0-9]{55}$/); + expect(result.stellarAddress).toBeTypeOf('string'); + if (result.memo) { + expect(['text', 'id', 'hash']).toContain(result.memo.type); + } + + const expectedAccount = process.env['FEDERATION_EXPECTED_ACCOUNT']; + if (expectedAccount) { + expect(result.accountId).toBe(expectedAccount); + } + }, 30_000); +});