@@ -3,10 +3,12 @@ import { Address, Base64, Hex } from 'ox'
33import { z } from 'zod'
44import * as Chat from '#/chat.ts'
55import * as AccountLink from '#/lib/accountLink.ts'
6+ import { formatAmount , formatCurrencyAmount , formatTipAmount } from '#/lib/format.ts'
67import * as hono from '#/lib/hono.ts'
78import * as Nanoid from '#/lib/nanoid.ts'
89import * as Slack from '#/lib/slack.ts'
910import * as Tempo from '#/lib/tempo.ts'
11+ import * as Tip from '#/lib/tip.ts'
1012import * as DB from '#db/client.ts'
1113
1214export const api = new Hono < {
@@ -56,14 +58,14 @@ export const api = new Hono<{
5658 return c . json ( {
5759 accessKeyAddress : link . access_key_address ,
5860 accessKeyExpiry : link . access_key_expires_at ,
59- accessKeyLimit : '10' ,
60- accessKeyLimitPeriodSeconds : 24 * 60 * 60 , // 1 day
61+ accessKeyLimit : AccountLink . reusableAccessKeyLimitText ,
62+ accessKeyLimitPeriodSeconds : AccountLink . reusableAccessKeyPeriodSeconds ,
6163 accessKeyPublicKey : link . access_key_public_key as `0x${string } `,
6264 chainId : link . chain_id ,
6365 chainName : Tempo . getChainName ( link . chain_id ) ,
6466 expiresAt : link . expires_at ,
6567 ok : true as const ,
66- tokenAddress : Address . checksum ( link . default_token_address ?? Tempo . pathUsdAddress ) ,
68+ tokenAddress : Address . checksum ( link . default_token_address ?? Tempo . addressLookup . pathUsd ) ,
6769 } )
6870 } )
6971 . post (
@@ -122,7 +124,7 @@ export const api = new Hono<{
122124 expiresAt : link . access_key_expires_at ,
123125 keyAuthorization : body . keyAuthorization ,
124126 rootAddress : body . address ,
125- tokenAddress : Address . checksum ( link . default_token_address ?? Tempo . pathUsdAddress ) ,
127+ tokenAddress : Address . checksum ( link . default_token_address ?? Tempo . addressLookup . pathUsd ) ,
126128 } )
127129 const now = new Date ( ) . toISOString ( )
128130 const existingAccount = await c . var . db
@@ -186,6 +188,17 @@ export const api = new Hono<{
186188 . deleteFrom ( 'access_key' )
187189 . where ( 'account_id' , '=' , account . id )
188190 . where ( 'chain_id' , '=' , link . chain_id )
191+ . where (
192+ 'token_address' ,
193+ '=' ,
194+ Address . checksum ( link . default_token_address ?? Tempo . addressLookup . pathUsd ) ,
195+ )
196+ . execute ( )
197+ await c . var . db
198+ . deleteFrom ( 'access_key' )
199+ . where ( 'account_id' , '=' , account . id )
200+ . where ( 'chain_id' , '=' , link . chain_id )
201+ . where ( 'token_address' , 'is' , null )
189202 . execute ( )
190203 await c . var . db
191204 . insertInto ( 'access_key' )
@@ -199,6 +212,9 @@ export const api = new Hono<{
199212 expires_at : link . access_key_expires_at ,
200213 id : Nanoid . generate ( ) ,
201214 revoked_at : null ,
215+ token_address : Address . checksum (
216+ link . default_token_address ?? Tempo . addressLookup . pathUsd ,
217+ ) ,
202218 updated_at : now ,
203219 } )
204220 . execute ( )
@@ -252,6 +268,131 @@ export const api = new Hono<{
252268 }
253269 } ,
254270 )
271+ . get ( '/api/confirm/:token' , async ( c ) => {
272+ try {
273+ const data = await Tip . getConfirmationData ( c . env , c . req . param ( 'token' ) )
274+ const metadata = Tempo . getTokenMetadataFallback ( data . payload . tokenAddress )
275+ return c . json ( {
276+ accessKeyAddress : data . accessKey . address ,
277+ accessKeyExpiry :
278+ data . payload . kind === 'reusable_access_key'
279+ ? ( data . payload . accessKeyExpiresAt ?? data . payload . expiresAt )
280+ : data . payload . expiresAt ,
281+ accessKeyLimit :
282+ data . payload . kind === 'reusable_access_key'
283+ ? AccountLink . reusableAccessKeyLimitText
284+ : formatAmount ( data . payload . amount ) ,
285+ accessKeyLimitPeriodSeconds :
286+ data . payload . kind === 'reusable_access_key'
287+ ? AccountLink . reusableAccessKeyPeriodSeconds
288+ : AccountLink . confirmationLinkTtlMs / 1000 ,
289+ accessKeyPublicKey : data . accessKey . publicKey as `0x${string } `,
290+ amount : formatAmount ( data . payload . amount ) ,
291+ chainId : data . payload . chainId ,
292+ kind : data . payload . kind ,
293+ memo : data . payload . memo ,
294+ ok : true as const ,
295+ recipientProviderUserId : data . payload . recipientProviderUserId ,
296+ tokenAddress : Address . checksum ( data . payload . tokenAddress ) ,
297+ tokenCurrency : metadata . currency ,
298+ tokenSymbol : metadata . symbol ,
299+ } )
300+ } catch ( error ) {
301+ return c . json (
302+ {
303+ code : 'invalid_confirmation' as const ,
304+ message : error instanceof Error ? error . message : 'Confirmation link expired' ,
305+ ok : false as const ,
306+ } ,
307+ 404 ,
308+ )
309+ }
310+ } )
311+ . post (
312+ '/api/confirm/:token' ,
313+ hono . validator (
314+ 'json' ,
315+ z . object ( {
316+ address : z . string ( ) . min ( 1 ) ,
317+ keyAuthorization : z . unknown ( ) ,
318+ } ) ,
319+ ) ,
320+ async ( c ) => {
321+ try {
322+ const body = c . req . valid ( 'json' )
323+ const data = await Tip . getConfirmationData ( c . env , c . req . param ( 'token' ) )
324+ const result = await Tip . confirmTipRequest ( c . env , {
325+ address : body . address ,
326+ keyAuthorization : body . keyAuthorization ,
327+ token : c . req . param ( 'token' ) ,
328+ } )
329+ if ( ! result . ok )
330+ return c . json (
331+ {
332+ code : result . code ,
333+ message : result . message ?? 'Payment failed.' ,
334+ ok : false as const ,
335+ } ,
336+ 400 ,
337+ )
338+
339+ if ( result . status === 'sent' )
340+ c . executionCtx . waitUntil (
341+ ( async ( ) => {
342+ const installation = await Chat . getSlack ( ) . getInstallation ( data . payload . providerId )
343+ if ( ! installation ) return
344+
345+ const amount = result . isDefaultToken
346+ ? formatCurrencyAmount ( result . amount , result . tokenCurrency )
347+ : formatTipAmount ( result . amount , result . tokenCurrency , result . tokenSymbol )
348+ const text = `<@${ result . senderProviderUserId } > ${ result . memo ? 'sent' : 'tipped' } <@${ result . recipientProviderUserId } > ${ amount } ${ result . memo ? ` for ${ result . memo } ` : '' } .`
349+ const body = new URLSearchParams ( )
350+ body . set (
351+ 'blocks' ,
352+ JSON . stringify ( [
353+ {
354+ text : {
355+ text : `${ text } <${ Tempo . formatTxLink ( result . chainId , result . transactionHash ) } |Receipt>` ,
356+ type : 'mrkdwn' ,
357+ } ,
358+ type : 'section' ,
359+ } ,
360+ ] ) ,
361+ )
362+ body . set ( 'channel' , data . payload . providerChannelId . replace ( / ^ s l a c k : / , '' ) )
363+ body . set ( 'text' , `${ text } Receipt` )
364+ body . set ( 'unfurl_links' , 'false' )
365+ body . set ( 'unfurl_media' , 'false' )
366+ const response = await Chat . getSlack ( ) . withBotToken ( installation . botToken , ( ) =>
367+ fetch ( `${ c . env . SLACK_API_URL } /chat.postMessage` , {
368+ body,
369+ headers : { authorization : `Bearer ${ installation . botToken } ` } ,
370+ method : 'POST' ,
371+ } ) ,
372+ )
373+ const json = z . parse (
374+ z . object ( {
375+ error : z . string ( ) . optional ( ) ,
376+ ok : z . boolean ( ) . optional ( ) ,
377+ } ) ,
378+ await response . json ( ) ,
379+ )
380+ if ( ! json . ok ) throw new Error ( json . error ?? 'Slack API chat.postMessage failed.' )
381+ } ) ( ) . catch ( console . error ) ,
382+ )
383+ return c . json ( { ok : true as const , transactionHash : result . transactionHash } )
384+ } catch ( error ) {
385+ return c . json (
386+ {
387+ code : 'confirmation_failed' as const ,
388+ message : error instanceof Error ? error . message : 'Payment failed.' ,
389+ ok : false as const ,
390+ } ,
391+ 400 ,
392+ )
393+ }
394+ } ,
395+ )
255396 . post ( '/api/chat/slack' , async ( c ) => {
256397 const request = c . req . raw
257398 const body = await request . text ( )
0 commit comments