From dd3e79a591fe9750ab6b27d74cca7d2799ecd42e Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 20 Jul 2026 17:34:25 -0400 Subject: [PATCH 1/3] refactor: use Tempo API fee sponsorship --- .env.example | 2 +- src/lib/tip.ts | 61 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 1eac111..21f8619 100644 --- a/.env.example +++ b/.env.example @@ -12,7 +12,7 @@ SLACK_APP_ID= # Printed by `pnpm slack:app create` SLACK_CLIENT_ID= # Printed by `pnpm slack:app create` SLACK_CLIENT_SECRET= # Printed by `pnpm slack:app create` SLACK_SIGNING_SECRET= # Printed by `pnpm slack:app create` -TEMPO_API_KEY= # Tempo API key with the data:read scope +TEMPO_API_KEY= # Tempo API key with data:read and rpc-relay:sponsor scopes TWITTER_ACCESS_TOKEN= # Twitter/X OAuth 1.0a access token for posting replies TWITTER_ACCESS_TOKEN_SECRET= # Twitter/X OAuth 1.0a access token secret for posting replies TWITTER_API_URL= # Defaults to https://api.twitter.com diff --git a/src/lib/tip.ts b/src/lib/tip.ts index d41b128..efd8801 100644 --- a/src/lib/tip.ts +++ b/src/lib/tip.ts @@ -11,14 +11,24 @@ import type { DB as Database } from '#db/types.gen.ts' import { AbiFunction, Address, Hash, Hex } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' import { KeyAuthorization } from 'ox/tempo' -import { BaseError, InsufficientFundsError, createClient, http } from 'viem' +import { + BaseError, + HttpRequestError, + InsufficientFundsError, + RpcRequestError, + TimeoutError, + createClient, + http, +} from 'viem' import { sendTransactionSync } from 'viem/actions' import { privateKeyToAccount } from 'viem/accounts' -import { Account as TempoAccount, Actions } from 'viem/tempo' +import { Account as TempoAccount, Actions, withRelay } from 'viem/tempo' import { getNodeError } from 'viem/utils' export { defaultReactionTipConfigs } from '#/lib/constants.ts' +const tempoSponsorUrl = 'https://api.tempo.xyz/rpc/sponsor' + export type TipResult = | { amount: string @@ -1955,10 +1965,26 @@ async function submitTipBatch( const account = TempoAccount.fromSecp256k1(input.accessKeyPrivateKey, { access: input.sender.account.address as `0x${string}`, }) + const chain = Tempo.getChain(input.workspace.chain_id) + const transport = http(Tempo.getRpcUrl(env, input.workspace.chain_id)) const client = createClient({ - chain: Tempo.getChain(input.workspace.chain_id), - transport: http(Tempo.getRpcUrl(env, input.workspace.chain_id)), + chain, + transport, }) + const sponsorClient = + input.workspace.chain_id === Tempo.chainLookup.localnet + ? client + : createClient({ + chain, + transport: withRelay( + transport, + http(tempoSponsorUrl, { + fetchOptions: { headers: { 'tempo-api-key': env.TEMPO_API_KEY } }, + retryCount: 0, + timeout: 5_000, // 5 seconds + }), + ), + }) const totalAmount = input.amount * input.connectedRecipients.length const balance = await Actions.token.getBalance(client, { account: input.sender.account.address as Address.Address, @@ -1988,7 +2014,13 @@ async function submitTipBatch( keyAuthorization: input.authorizationUsedAt ? undefined : input.keyAuthorization, nonceKey: 'expiring' as const, } - if (!feePayerPrivateKey) + const feePayer = + input.workspace.chain_id === Tempo.chainLookup.localnet + ? feePayerPrivateKey + ? privateKeyToAccount(feePayerPrivateKey) + : null + : true + if (!feePayer) return [ await sendTransactionSync(client, { ...parameters, @@ -1999,14 +2031,14 @@ async function submitTipBatch( try { return [ - await sendTransactionSync(client, { + await sendTransactionSync(sponsorClient, { ...parameters, - feePayer: privateKeyToAccount(feePayerPrivateKey), + feePayer, } as never), 'sponsor' as const, ] as const } catch (error) { - if (!isInsufficientFundsError(error)) throw error + if (!isInsufficientFundsError(error) && !isTempoSponsorError(error)) throw error return [ await sendTransactionSync(client, { ...parameters, @@ -2671,6 +2703,19 @@ function isInsufficientFundsError(error: unknown) { return false } +function isTempoSponsorError(error: unknown) { + if (!(error instanceof BaseError)) return false + return Boolean( + error.walk( + (cause) => + (cause instanceof HttpRequestError || + cause instanceof RpcRequestError || + cause instanceof TimeoutError) && + cause.url === tempoSponsorUrl, + ), + ) +} + function isUniqueConstraintError(error: unknown) { return error instanceof Error && /unique constraint|constraint failed/i.test(error.message) } From 2655d61218bced8eb3351d54bba717e9d57a9ada Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 20 Jul 2026 17:39:22 -0400 Subject: [PATCH 2/3] chore: update brace-expansion --- pnpm-lock.yaml | 11 ++++++----- pnpm-workspace.yaml | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ee3e32..e9e7be3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,7 @@ overrides: '@babel/core': 7.29.6 '@grpc/grpc-js': 1.14.4 axios: 1.18.0 + brace-expansion: 2.1.2 esbuild: 0.28.1 form-data: 4.0.6 hono: 4.12.25 @@ -3139,8 +3140,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@2.1.0: - resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -9087,7 +9088,7 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@2.1.0: + brace-expansion@2.1.2: dependencies: balanced-match: 1.0.2 @@ -10875,11 +10876,11 @@ snapshots: minimatch@5.1.9: dependencies: - brace-expansion: 2.1.0 + brace-expansion: 2.1.2 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.0 + brace-expansion: 2.1.2 minimist@1.2.8: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8549e14..04923a0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -55,6 +55,7 @@ overrides: '@babel/core': 7.29.6 '@grpc/grpc-js': 1.14.4 axios: 1.18.0 + brace-expansion: 2.1.2 esbuild: 0.28.1 form-data: 4.0.6 hono: 4.12.25 From 1905802d600f8aeefe88f591cb10f8f881eb6588 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 20 Jul 2026 17:44:15 -0400 Subject: [PATCH 3/3] fix: retry Slack preview icon setup --- .github/workflows/preview_deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview_deploy.yml b/.github/workflows/preview_deploy.yml index 6d2f528..fa908e2 100644 --- a/.github/workflows/preview_deploy.yml +++ b/.github/workflows/preview_deploy.yml @@ -462,7 +462,7 @@ jobs: APP_ID: ${{ steps.slack.outputs.app_id }} SLACK_CONFIG_ACCESS_TOKEN: ${{ steps.slack_token.outputs.token }} run: | - for attempt in 1 2 3; do + for attempt in 1 2 3 4 5 6; do RESPONSE=$(curl -fsS -X POST https://slack.com/api/apps.icon.set \ -H "Authorization: Bearer ${SLACK_CONFIG_ACCESS_TOKEN}" \ -H "Content-Type: application/x-www-form-urlencoded" \ @@ -470,7 +470,7 @@ jobs: --data-urlencode "url=https://${PREVIEW_HOST}/tipbot-preview.png") OK=$(echo "$RESPONSE" | jq -r '.ok') ERROR=$(echo "$RESPONSE" | jq -r '.error // empty') - if [ "$OK" = "true" ] || [ "$ERROR" != "internal_error" ] || [ "$attempt" = "3" ]; then break; fi + if [ "$OK" = "true" ] || { [ "$ERROR" != "internal_error" ] && [ "$ERROR" != "icon_not_accessible" ]; } || [ "$attempt" = "6" ]; then break; fi sleep 5 # 5 seconds done OK=$(echo "$RESPONSE" | jq -r '.ok')