Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/preview_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -462,15 +462,15 @@ 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" \
--data-urlencode "app_id=${APP_ID}" \
--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')
Expand Down
11 changes: 6 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 53 additions & 8 deletions src/lib/tip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
Loading