@akta/algomd-rn 0.0.1-canary
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +46 -0
- package/src/components/ASA.tsx +221 -0
- package/src/components/ASASearch.tsx +79 -0
- package/src/components/Account.tsx +150 -0
- package/src/components/AccountSearch.tsx +75 -0
- package/src/components/AuctionListing.tsx +220 -0
- package/src/components/NFDProfile.tsx +173 -0
- package/src/components/NFDSearch.tsx +81 -0
- package/src/components/NFTListing.tsx +181 -0
- package/src/components/NFTSearch.tsx +68 -0
- package/src/components/Poll.tsx +234 -0
- package/src/components/PollSearch.tsx +79 -0
- package/src/components/RaffleListing.tsx +204 -0
- package/src/components/SelfFetching.tsx +358 -0
- package/src/components/TradeOffer.tsx +245 -0
- package/src/components/TradeSearch.tsx +103 -0
- package/src/components/TransactionDetails.tsx +242 -0
- package/src/components/TransactionSearch.tsx +102 -0
- package/src/components/index.ts +30 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useAlgomdData.ts +445 -0
- package/src/hooks/useAlgorandClient.ts +39 -0
- package/src/index.ts +83 -0
- package/src/provider/AlgomdProvider.tsx +45 -0
- package/src/provider/context.ts +18 -0
- package/src/provider/imageResolver.ts +12 -0
- package/src/provider/index.ts +5 -0
- package/src/provider/networks.ts +23 -0
- package/src/provider/types.ts +17 -0
- package/src/types/algorand.ts +205 -0
- package/src/types/index.ts +1 -0
- package/src/ui/CopyButton.tsx +40 -0
- package/src/ui/DataStates.tsx +41 -0
- package/src/ui/ProgressBar.tsx +42 -0
- package/src/ui/SearchSheet.tsx +134 -0
- package/src/ui/SizeContainer.tsx +25 -0
- package/src/ui/StatusBadge.tsx +51 -0
- package/src/ui/index.ts +6 -0
- package/src/utils/format.ts +70 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/search.ts +65 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AlgoMD self-fetching data hooks.
|
|
3
|
+
*
|
|
4
|
+
* Each hook reads directly from algod/indexer (via @akta/sdk typed clients
|
|
5
|
+
* where available) and maps to the algomd-rn display types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// These web APIs are available in Hermes / all modern JS engines but not in TS "esnext" lib
|
|
9
|
+
declare const TextDecoder: { new (): { decode(input?: ArrayBuffer | Uint8Array): string } }
|
|
10
|
+
declare function btoa(data: string): string
|
|
11
|
+
|
|
12
|
+
import { useQuery } from '@tanstack/react-query'
|
|
13
|
+
import { useAlgorandClient, useIndexerClient } from './useAlgorandClient'
|
|
14
|
+
import { useAlgomd } from '../provider/context'
|
|
15
|
+
import type { AlgorandClient } from '@algorandfoundation/algokit-utils'
|
|
16
|
+
import type {
|
|
17
|
+
AlgorandAccount,
|
|
18
|
+
ASA as ASAType,
|
|
19
|
+
TransactionDetails as TransactionDetailsType,
|
|
20
|
+
NFDProfile as NFDProfileType,
|
|
21
|
+
Poll as PollType,
|
|
22
|
+
PollOption,
|
|
23
|
+
RaffleListing as RaffleListingType,
|
|
24
|
+
AuctionListing as AuctionListingType,
|
|
25
|
+
TradeOffer as TradeOfferType,
|
|
26
|
+
NFTListing as NFTListingType,
|
|
27
|
+
} from '../types/algorand'
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Shared helper: fetch ASA details from algod
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
async function fetchASADetails(algorand: AlgorandClient, assetId: number | bigint): Promise<ASAType> {
|
|
34
|
+
try {
|
|
35
|
+
const info = await algorand.client.algod.getAssetByID(BigInt(assetId)).do()
|
|
36
|
+
const params = info.params
|
|
37
|
+
return {
|
|
38
|
+
id: Number(assetId),
|
|
39
|
+
name: params.name ?? `Asset #${assetId}`,
|
|
40
|
+
unitName: params.unitName ?? '',
|
|
41
|
+
total: Number(params.total),
|
|
42
|
+
decimals: Number(params.decimals),
|
|
43
|
+
defaultFrozen: params.defaultFrozen ?? false,
|
|
44
|
+
url: params.url,
|
|
45
|
+
creator: params.creator ?? '',
|
|
46
|
+
createdAt: new Date(),
|
|
47
|
+
verified: false,
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
return {
|
|
51
|
+
id: Number(assetId),
|
|
52
|
+
name: `Asset #${assetId}`,
|
|
53
|
+
unitName: '???',
|
|
54
|
+
total: 0,
|
|
55
|
+
decimals: 0,
|
|
56
|
+
defaultFrozen: false,
|
|
57
|
+
creator: '',
|
|
58
|
+
createdAt: new Date(),
|
|
59
|
+
verified: false,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// On-chain hooks (algod / indexer)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export function useAccountData(address: string | undefined) {
|
|
69
|
+
const algorand = useAlgorandClient()
|
|
70
|
+
return useQuery({
|
|
71
|
+
queryKey: ['algomd', 'account', address],
|
|
72
|
+
queryFn: async (): Promise<AlgorandAccount> => {
|
|
73
|
+
if (!address) throw new Error('No address provided')
|
|
74
|
+
const info = await algorand.client.algod.accountInformation(address).do()
|
|
75
|
+
const assets: ASAType[] = (info.assets ?? []).map(
|
|
76
|
+
(a: { assetId: bigint; amount: bigint }) => ({
|
|
77
|
+
id: Number(a.assetId),
|
|
78
|
+
name: `Asset #${a.assetId}`,
|
|
79
|
+
unitName: '',
|
|
80
|
+
total: Number(a.amount),
|
|
81
|
+
decimals: 0,
|
|
82
|
+
defaultFrozen: false,
|
|
83
|
+
creator: '',
|
|
84
|
+
createdAt: new Date(),
|
|
85
|
+
verified: false,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
return {
|
|
89
|
+
id: address,
|
|
90
|
+
address,
|
|
91
|
+
balance: Number(info.amount),
|
|
92
|
+
assets,
|
|
93
|
+
apps: (info.appsLocalState ?? []).map(
|
|
94
|
+
(a: { id: bigint }) => ({
|
|
95
|
+
id: Number(a.id),
|
|
96
|
+
creator: '',
|
|
97
|
+
globalState: {},
|
|
98
|
+
localState: {},
|
|
99
|
+
params: {
|
|
100
|
+
globalNumUint: 0,
|
|
101
|
+
globalNumByteSlice: 0,
|
|
102
|
+
localNumUint: 0,
|
|
103
|
+
localNumByteSlice: 0,
|
|
104
|
+
},
|
|
105
|
+
createdAt: new Date(),
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
createdAt: new Date(),
|
|
109
|
+
isOnline: info.status === 'Online',
|
|
110
|
+
round: Number(info.round ?? 0),
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
enabled: !!address,
|
|
114
|
+
staleTime: 60_000,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function useASAData(assetId: number | string | undefined) {
|
|
119
|
+
const algorand = useAlgorandClient()
|
|
120
|
+
return useQuery({
|
|
121
|
+
queryKey: ['algomd', 'asa', assetId],
|
|
122
|
+
queryFn: async (): Promise<ASAType> => {
|
|
123
|
+
if (!assetId) throw new Error('No asset ID provided')
|
|
124
|
+
return fetchASADetails(algorand, Number(assetId))
|
|
125
|
+
},
|
|
126
|
+
enabled: assetId != null,
|
|
127
|
+
staleTime: 60_000,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function useTransactionData(txId: string | undefined) {
|
|
132
|
+
const indexer = useIndexerClient()
|
|
133
|
+
return useQuery({
|
|
134
|
+
queryKey: ['algomd', 'transaction', txId],
|
|
135
|
+
queryFn: async (): Promise<TransactionDetailsType> => {
|
|
136
|
+
if (!txId) throw new Error('No transaction ID provided')
|
|
137
|
+
if (!indexer) throw new Error('Indexer not configured')
|
|
138
|
+
const result = await indexer.lookupTransactionByID(txId).do()
|
|
139
|
+
const txn = result.transaction
|
|
140
|
+
|
|
141
|
+
const typeMap: Record<string, TransactionDetailsType['type']> = {
|
|
142
|
+
pay: 'payment',
|
|
143
|
+
axfer: 'asset-transfer',
|
|
144
|
+
appl: 'application-call',
|
|
145
|
+
acfg: 'asset-config',
|
|
146
|
+
keyreg: 'key-registration',
|
|
147
|
+
afrz: 'asset-freeze',
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// algosdk v3 indexer returns note as Uint8Array, not base64 string
|
|
151
|
+
let note: string | undefined
|
|
152
|
+
if (txn.note) {
|
|
153
|
+
try {
|
|
154
|
+
note = new TextDecoder().decode(txn.note)
|
|
155
|
+
} catch {
|
|
156
|
+
note = undefined
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
id: txId,
|
|
162
|
+
type: typeMap[txn.txType ?? ''] ?? 'payment',
|
|
163
|
+
from: txn.sender,
|
|
164
|
+
to: txn.paymentTransaction?.receiver ?? txn.assetTransferTransaction?.receiver,
|
|
165
|
+
amount: Number(
|
|
166
|
+
txn.paymentTransaction?.amount ?? txn.assetTransferTransaction?.amount ?? 0,
|
|
167
|
+
),
|
|
168
|
+
fee: Number(txn.fee),
|
|
169
|
+
round: Number(txn.confirmedRound),
|
|
170
|
+
timestamp: new Date((txn.roundTime ?? 0) * 1000),
|
|
171
|
+
confirmed: !!txn.confirmedRound,
|
|
172
|
+
signature: (() => {
|
|
173
|
+
const sig = txn.signature?.sig
|
|
174
|
+
if (!sig) return ''
|
|
175
|
+
if (typeof sig === 'string') return sig
|
|
176
|
+
// Convert Uint8Array to base64 without Node Buffer
|
|
177
|
+
return btoa(Array.from(sig as Uint8Array, (b: number) => String.fromCharCode(b)).join(''))
|
|
178
|
+
})(),
|
|
179
|
+
note,
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
enabled: !!txId && !!indexer,
|
|
183
|
+
staleTime: 120_000,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function useNFDProfileData(name: string | undefined) {
|
|
188
|
+
const { config } = useAlgomd()
|
|
189
|
+
const nfdApiUrl = config.nfdApiUrl ?? 'https://api.nf.domains'
|
|
190
|
+
return useQuery({
|
|
191
|
+
queryKey: ['algomd', 'nfd', name],
|
|
192
|
+
queryFn: async (): Promise<NFDProfileType> => {
|
|
193
|
+
if (!name) throw new Error('No NFD name provided')
|
|
194
|
+
const response = await fetch(`${nfdApiUrl}/nfd/${encodeURIComponent(name)}`)
|
|
195
|
+
if (!response.ok) throw new Error(`NFD lookup failed: ${response.status}`)
|
|
196
|
+
const nfd = await response.json()
|
|
197
|
+
return {
|
|
198
|
+
id: nfd.appID?.toString() ?? name,
|
|
199
|
+
name: nfd.name ?? name,
|
|
200
|
+
address: nfd.depositAccount ?? nfd.caAlgo?.[0] ?? '',
|
|
201
|
+
avatar: nfd.properties?.userDefined?.avatar,
|
|
202
|
+
bio: nfd.properties?.userDefined?.bio,
|
|
203
|
+
properties: nfd.properties?.userDefined ?? {},
|
|
204
|
+
verified: nfd.properties?.verified?.caAlgo === nfd.caAlgo?.[0],
|
|
205
|
+
createdAt: new Date(nfd.timeCreated ?? Date.now()),
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
enabled: !!name,
|
|
209
|
+
staleTime: 120_000,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// On-chain contract hooks (via @akta/sdk typed clients)
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
export function usePollData(appId: number | undefined) {
|
|
218
|
+
const algorand = useAlgorandClient()
|
|
219
|
+
return useQuery({
|
|
220
|
+
queryKey: ['algomd', 'poll', appId],
|
|
221
|
+
queryFn: async (): Promise<PollType> => {
|
|
222
|
+
if (!appId) throw new Error('No poll app ID provided')
|
|
223
|
+
const { PollSDK } = await import('@akta/sdk')
|
|
224
|
+
const sdk = new PollSDK({
|
|
225
|
+
algorand,
|
|
226
|
+
factoryParams: { appId: BigInt(appId) },
|
|
227
|
+
})
|
|
228
|
+
const state = await sdk.state()
|
|
229
|
+
|
|
230
|
+
const optionTexts = [state.question, state.optionOne, state.optionTwo, state.optionThree, state.optionFour, state.optionFive]
|
|
231
|
+
const optionVotes = [state.votesOne, state.votesTwo, state.votesThree, state.votesFour, state.votesFive]
|
|
232
|
+
const options: PollOption[] = []
|
|
233
|
+
const count = Number(state.optionCount)
|
|
234
|
+
for (let i = 0; i < count && i < 5; i++) {
|
|
235
|
+
options.push({
|
|
236
|
+
id: `${i + 1}`,
|
|
237
|
+
text: optionTexts[i + 1] || `Option ${i + 1}`,
|
|
238
|
+
votes: Number(optionVotes[i]),
|
|
239
|
+
votingPower: Number(optionVotes[i]),
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const endTime = Number(state.endTime)
|
|
244
|
+
const now = Date.now() / 1000
|
|
245
|
+
const isExpired = endTime > 0 && endTime < now
|
|
246
|
+
const totalVotes = optionVotes.slice(0, count).reduce((sum, v) => sum + Number(v), 0)
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
id: appId.toString(),
|
|
250
|
+
question: state.question,
|
|
251
|
+
options,
|
|
252
|
+
creator: '',
|
|
253
|
+
createdAt: new Date(),
|
|
254
|
+
expiresAt: endTime > 0 ? new Date(endTime * 1000) : undefined,
|
|
255
|
+
totalVotes,
|
|
256
|
+
status: isExpired ? 'ended' : 'active',
|
|
257
|
+
gating: Number(state.gateId) > 0
|
|
258
|
+
? { type: 'asset-holding' as const, requirements: { assets: [{ assetId: Number(state.gateId), minimumBalance: 1 }] } }
|
|
259
|
+
: undefined,
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
enabled: appId != null,
|
|
263
|
+
staleTime: 30_000,
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function useRaffleData(appId: number | undefined) {
|
|
268
|
+
const algorand = useAlgorandClient()
|
|
269
|
+
return useQuery({
|
|
270
|
+
queryKey: ['algomd', 'raffle', appId],
|
|
271
|
+
queryFn: async (): Promise<RaffleListingType> => {
|
|
272
|
+
if (!appId) throw new Error('No raffle app ID provided')
|
|
273
|
+
const { RaffleSDK } = await import('@akta/sdk')
|
|
274
|
+
const sdk = new RaffleSDK({
|
|
275
|
+
algorand,
|
|
276
|
+
factoryParams: { appId: BigInt(appId) },
|
|
277
|
+
})
|
|
278
|
+
const state = await sdk.state()
|
|
279
|
+
|
|
280
|
+
const [prizeAsset, entryAsset] = await Promise.all([
|
|
281
|
+
fetchASADetails(algorand, Number(state.prize)),
|
|
282
|
+
fetchASADetails(algorand, Number(state.ticketAsset)),
|
|
283
|
+
])
|
|
284
|
+
|
|
285
|
+
const startTs = Number(state.startTimestamp)
|
|
286
|
+
const endTs = Number(state.endTimestamp)
|
|
287
|
+
const now = Date.now() / 1000
|
|
288
|
+
let status: RaffleListingType['status']
|
|
289
|
+
if (startTs > now) status = 'upcoming'
|
|
290
|
+
else if (endTs > now) status = 'active'
|
|
291
|
+
else status = 'ended'
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
id: appId.toString(),
|
|
295
|
+
title: `Raffle #${appId}`,
|
|
296
|
+
description: `Win ${prizeAsset.name}! Entry costs ${entryAsset.unitName || 'tokens'}.`,
|
|
297
|
+
pricePerEntry: 1,
|
|
298
|
+
entryAsset,
|
|
299
|
+
startTime: new Date(startTs * 1000),
|
|
300
|
+
endTime: new Date(endTs * 1000),
|
|
301
|
+
prizes: [prizeAsset],
|
|
302
|
+
entryCount: Number(state.entryCount),
|
|
303
|
+
ticketCount: Number(state.maxTickets),
|
|
304
|
+
creator: state.seller ?? '',
|
|
305
|
+
status,
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
enabled: appId != null,
|
|
309
|
+
staleTime: 30_000,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function useAuctionData(appId: number | undefined) {
|
|
314
|
+
const algorand = useAlgorandClient()
|
|
315
|
+
return useQuery({
|
|
316
|
+
queryKey: ['algomd', 'auction', appId],
|
|
317
|
+
queryFn: async (): Promise<AuctionListingType> => {
|
|
318
|
+
if (!appId) throw new Error('No auction app ID provided')
|
|
319
|
+
const { AuctionSDK } = await import('@akta/sdk')
|
|
320
|
+
const sdk = new AuctionSDK({
|
|
321
|
+
algorand,
|
|
322
|
+
factoryParams: { appId: BigInt(appId) },
|
|
323
|
+
})
|
|
324
|
+
const state = await sdk.state()
|
|
325
|
+
|
|
326
|
+
const [prizeAsset, bidAsset] = await Promise.all([
|
|
327
|
+
fetchASADetails(algorand, Number(state.prize)),
|
|
328
|
+
fetchASADetails(algorand, Number(state.bidAsset)),
|
|
329
|
+
])
|
|
330
|
+
|
|
331
|
+
const startTs = Number(state.startTimestamp)
|
|
332
|
+
const endTs = Number(state.endTimestamp)
|
|
333
|
+
const now = Date.now() / 1000
|
|
334
|
+
let status: AuctionListingType['status']
|
|
335
|
+
if (startTs > now) status = 'upcoming'
|
|
336
|
+
else if (endTs > now) status = 'active'
|
|
337
|
+
else status = 'ended'
|
|
338
|
+
|
|
339
|
+
const bidDecimals = bidAsset.decimals || 0
|
|
340
|
+
const divisor = Math.pow(10, bidDecimals)
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
id: appId.toString(),
|
|
344
|
+
title: `Auction #${appId}`,
|
|
345
|
+
description: `Bid on ${prizeAsset.name} with ${bidAsset.unitName || 'tokens'}.`,
|
|
346
|
+
bidAsset,
|
|
347
|
+
currentHighestBid: Number(state.highestBid) / divisor,
|
|
348
|
+
minimumNextBid: (Number(state.highestBid) + Number(state.bidMinimumIncrease)) / divisor,
|
|
349
|
+
startTime: new Date(startTs * 1000),
|
|
350
|
+
endTime: new Date(endTs * 1000),
|
|
351
|
+
prizes: [prizeAsset],
|
|
352
|
+
bidFeePercentage: Number(state.bidFee) > 0 ? Number(state.bidFee) / 100 : undefined,
|
|
353
|
+
currentBidFeePool: 0,
|
|
354
|
+
bidCount: Number(state.bidID),
|
|
355
|
+
timeExtended: false,
|
|
356
|
+
creator: state.seller ?? '',
|
|
357
|
+
status,
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
enabled: appId != null,
|
|
361
|
+
staleTime: 30_000,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function useNFTListingData(appId: number | undefined) {
|
|
366
|
+
const algorand = useAlgorandClient()
|
|
367
|
+
return useQuery({
|
|
368
|
+
queryKey: ['algomd', 'nftlisting', appId],
|
|
369
|
+
queryFn: async (): Promise<NFTListingType> => {
|
|
370
|
+
if (!appId) throw new Error('No NFT listing app ID provided')
|
|
371
|
+
const { MarketplaceSDK } = await import('@akta/sdk')
|
|
372
|
+
const marketplace = new MarketplaceSDK({
|
|
373
|
+
algorand,
|
|
374
|
+
factoryParams: {},
|
|
375
|
+
})
|
|
376
|
+
const listing = marketplace.getListing({ appId: BigInt(appId) })
|
|
377
|
+
const state = await listing.state()
|
|
378
|
+
|
|
379
|
+
const [nftAsset, paymentAsset] = await Promise.all([
|
|
380
|
+
fetchASADetails(algorand, Number(state.prize)),
|
|
381
|
+
fetchASADetails(algorand, Number(state.paymentAsset)),
|
|
382
|
+
])
|
|
383
|
+
|
|
384
|
+
const payDecimals = paymentAsset.decimals || 0
|
|
385
|
+
const divisor = Math.pow(10, payDecimals)
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
id: appId.toString(),
|
|
389
|
+
nft: nftAsset,
|
|
390
|
+
price: Number(state.price) / divisor,
|
|
391
|
+
priceAsset: paymentAsset,
|
|
392
|
+
currency: paymentAsset.unitName || 'ALGO',
|
|
393
|
+
seller: state.seller ?? '',
|
|
394
|
+
authenticityBadge: false,
|
|
395
|
+
quantity: 1,
|
|
396
|
+
reservedFor: state.reservedFor && state.reservedFor !== 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ' ? state.reservedFor : undefined,
|
|
397
|
+
views: 0,
|
|
398
|
+
favorites: 0,
|
|
399
|
+
createdAt: new Date(),
|
|
400
|
+
listedAt: new Date(),
|
|
401
|
+
expiresAt: Number(state.expiration) > 0 ? new Date(Number(state.expiration) * 1000) : undefined,
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
enabled: appId != null,
|
|
405
|
+
staleTime: 60_000,
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function useTradeOfferData(appId: number | undefined, offerId: number | undefined) {
|
|
410
|
+
const algorand = useAlgorandClient()
|
|
411
|
+
return useQuery({
|
|
412
|
+
queryKey: ['algomd', 'trade', appId, offerId],
|
|
413
|
+
queryFn: async (): Promise<TradeOfferType> => {
|
|
414
|
+
if (appId == null || offerId == null) throw new Error('No trade offer ID provided')
|
|
415
|
+
const { HyperSwapSDK } = await import('@akta/sdk')
|
|
416
|
+
const sdk = new HyperSwapSDK({
|
|
417
|
+
algorand,
|
|
418
|
+
factoryParams: { appId: BigInt(appId) },
|
|
419
|
+
})
|
|
420
|
+
const offer = await sdk.getOffer({ id: BigInt(offerId) })
|
|
421
|
+
|
|
422
|
+
const stateMap: Record<number, TradeOfferType['status']> = {
|
|
423
|
+
10: 'pending', // Offered
|
|
424
|
+
20: 'pending', // Escrowing
|
|
425
|
+
30: 'pending', // Disbursing
|
|
426
|
+
40: 'accepted', // Completed
|
|
427
|
+
50: 'expired', // Cancelled
|
|
428
|
+
60: 'expired', // CancelCompleted
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
id: offerId.toString(),
|
|
433
|
+
creator: '',
|
|
434
|
+
recipients: [],
|
|
435
|
+
offering: [],
|
|
436
|
+
requesting: [],
|
|
437
|
+
expiresAt: new Date(Number(offer.expiration) * 1000),
|
|
438
|
+
status: stateMap[offer.state] ?? 'pending',
|
|
439
|
+
createdAt: new Date(),
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
enabled: appId != null && offerId != null,
|
|
443
|
+
staleTime: 60_000,
|
|
444
|
+
})
|
|
445
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { AlgorandClient } from '@algorandfoundation/algokit-utils'
|
|
3
|
+
import algosdk from 'algosdk'
|
|
4
|
+
import { useAlgomd } from '../provider/context'
|
|
5
|
+
import { NETWORK_DEFAULTS } from '../provider/networks'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a memoized AlgorandClient from provider config.
|
|
9
|
+
*/
|
|
10
|
+
export function useAlgorandClient(): AlgorandClient {
|
|
11
|
+
const { config } = useAlgomd()
|
|
12
|
+
|
|
13
|
+
return useMemo(() => {
|
|
14
|
+
const defaults = NETWORK_DEFAULTS[config.network]
|
|
15
|
+
const server = config.algodServer ?? defaults.algodServer
|
|
16
|
+
const port = config.algodPort ?? defaults.algodPort
|
|
17
|
+
const token = config.algodToken ?? defaults.algodToken ?? ''
|
|
18
|
+
|
|
19
|
+
return AlgorandClient.fromConfig({
|
|
20
|
+
algodConfig: { server, port, token },
|
|
21
|
+
})
|
|
22
|
+
}, [config.network, config.algodServer, config.algodPort, config.algodToken])
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a memoized Indexer client from provider config.
|
|
27
|
+
* Returns null if indexer is not configured.
|
|
28
|
+
*/
|
|
29
|
+
export function useIndexerClient(): algosdk.Indexer | null {
|
|
30
|
+
const { config } = useAlgomd()
|
|
31
|
+
|
|
32
|
+
return useMemo(() => {
|
|
33
|
+
const defaults = NETWORK_DEFAULTS[config.network]
|
|
34
|
+
const server = config.indexerServer ?? defaults.indexerServer
|
|
35
|
+
if (!server) return null
|
|
36
|
+
const token = config.indexerToken ?? defaults.indexerToken ?? ''
|
|
37
|
+
return new algosdk.Indexer(token, server, '')
|
|
38
|
+
}, [config.network, config.indexerServer, config.indexerToken])
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Provider
|
|
2
|
+
export { AlgomdProvider, useAlgomd, defaultResolveImageUrl, NETWORK_DEFAULTS } from './provider';
|
|
3
|
+
export type { AlgomdConfig, AlgomdNetwork } from './provider';
|
|
4
|
+
|
|
5
|
+
// Self-fetching Components
|
|
6
|
+
export {
|
|
7
|
+
Account,
|
|
8
|
+
ASA,
|
|
9
|
+
NFTListing,
|
|
10
|
+
NFDProfile,
|
|
11
|
+
TransactionDetails,
|
|
12
|
+
Poll,
|
|
13
|
+
RaffleListing,
|
|
14
|
+
AuctionListing,
|
|
15
|
+
TradeOffer,
|
|
16
|
+
// Display-only (no self-fetch)
|
|
17
|
+
AccountDisplay,
|
|
18
|
+
ASADisplay,
|
|
19
|
+
NFTListingDisplay,
|
|
20
|
+
NFDProfileDisplay,
|
|
21
|
+
TransactionDetailsDisplay,
|
|
22
|
+
PollDisplay,
|
|
23
|
+
RaffleListingDisplay,
|
|
24
|
+
AuctionListingDisplay,
|
|
25
|
+
TradeOfferDisplay,
|
|
26
|
+
// Search
|
|
27
|
+
AccountSearch,
|
|
28
|
+
ASASearch,
|
|
29
|
+
NFTSearch,
|
|
30
|
+
NFDSearch,
|
|
31
|
+
TransactionSearch,
|
|
32
|
+
PollSearch,
|
|
33
|
+
TradeSearch,
|
|
34
|
+
} from './components';
|
|
35
|
+
|
|
36
|
+
// Hooks
|
|
37
|
+
export {
|
|
38
|
+
useAlgorandClient,
|
|
39
|
+
useIndexerClient,
|
|
40
|
+
useAccountData,
|
|
41
|
+
useASAData,
|
|
42
|
+
useTransactionData,
|
|
43
|
+
useNFDProfileData,
|
|
44
|
+
usePollData,
|
|
45
|
+
useRaffleData,
|
|
46
|
+
useAuctionData,
|
|
47
|
+
useNFTListingData,
|
|
48
|
+
useTradeOfferData,
|
|
49
|
+
} from './hooks';
|
|
50
|
+
|
|
51
|
+
// Shared UI
|
|
52
|
+
export { SizeContainer, CopyButton, StatusBadge, ProgressBar, SearchSheet } from './ui';
|
|
53
|
+
export { LoadingSkeleton, ErrorState } from './ui/DataStates';
|
|
54
|
+
|
|
55
|
+
// Types
|
|
56
|
+
export type {
|
|
57
|
+
AlgorandAccount,
|
|
58
|
+
ASA as ASAType,
|
|
59
|
+
NFTListing as NFTListingType,
|
|
60
|
+
NFDProfile as NFDProfileType,
|
|
61
|
+
TransactionDetails as TransactionDetailsType,
|
|
62
|
+
RaffleListing as RaffleListingType,
|
|
63
|
+
AuctionListing as AuctionListingType,
|
|
64
|
+
TradeOffer as TradeOfferType,
|
|
65
|
+
Poll as PollType,
|
|
66
|
+
PollOption,
|
|
67
|
+
GatingInfo,
|
|
68
|
+
Application,
|
|
69
|
+
ComponentSize,
|
|
70
|
+
SearchResult,
|
|
71
|
+
SearchableEntity,
|
|
72
|
+
} from './types';
|
|
73
|
+
|
|
74
|
+
// Utilities
|
|
75
|
+
export {
|
|
76
|
+
formatAddress,
|
|
77
|
+
formatNumber,
|
|
78
|
+
formatCurrency,
|
|
79
|
+
formatDate,
|
|
80
|
+
formatRelativeTime,
|
|
81
|
+
formatAssetAmount,
|
|
82
|
+
searchEntities,
|
|
83
|
+
} from './utils';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
3
|
+
import { AlgomdContext, type AlgomdContextValue } from './context'
|
|
4
|
+
import type { AlgomdConfig } from './types'
|
|
5
|
+
import { defaultResolveImageUrl } from './imageResolver'
|
|
6
|
+
|
|
7
|
+
interface AlgomdProviderProps {
|
|
8
|
+
config: AlgomdConfig
|
|
9
|
+
/** Share cache with consuming app's react-query. If omitted, creates an internal one. */
|
|
10
|
+
queryClient?: QueryClient
|
|
11
|
+
children: React.ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const defaultQueryClient = new QueryClient({
|
|
15
|
+
defaultOptions: {
|
|
16
|
+
queries: {
|
|
17
|
+
staleTime: 60_000,
|
|
18
|
+
retry: 2,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export function AlgomdProvider({ config, queryClient, children }: AlgomdProviderProps) {
|
|
24
|
+
const contextValue = useMemo<AlgomdContextValue>(() => ({
|
|
25
|
+
config,
|
|
26
|
+
resolveImageUrl: config.resolveImageUrl ?? defaultResolveImageUrl,
|
|
27
|
+
}), [config])
|
|
28
|
+
|
|
29
|
+
const content = (
|
|
30
|
+
<AlgomdContext.Provider value={contextValue}>
|
|
31
|
+
{children}
|
|
32
|
+
</AlgomdContext.Provider>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// If a queryClient is provided (shared), don't wrap with another QueryClientProvider
|
|
36
|
+
if (queryClient) {
|
|
37
|
+
return content
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<QueryClientProvider client={defaultQueryClient}>
|
|
42
|
+
{content}
|
|
43
|
+
</QueryClientProvider>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import type { AlgomdConfig } from './types'
|
|
3
|
+
import { defaultResolveImageUrl } from './imageResolver'
|
|
4
|
+
|
|
5
|
+
export interface AlgomdContextValue {
|
|
6
|
+
config: AlgomdConfig
|
|
7
|
+
resolveImageUrl: (src: string, width: number, quality?: number) => string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const AlgomdContext = createContext<AlgomdContextValue | null>(null)
|
|
11
|
+
|
|
12
|
+
export function useAlgomd(): AlgomdContextValue {
|
|
13
|
+
const ctx = useContext(AlgomdContext)
|
|
14
|
+
if (!ctx) {
|
|
15
|
+
throw new Error('useAlgomd must be used within an <AlgomdProvider>')
|
|
16
|
+
}
|
|
17
|
+
return ctx
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default image URL resolver using Akita CDN proxy.
|
|
3
|
+
* Transforms IPFS and HTTP(S) URLs into optimized CDN URLs.
|
|
4
|
+
*/
|
|
5
|
+
export function defaultResolveImageUrl(src: string, width: number, quality = 75): string {
|
|
6
|
+
if (!src.startsWith('ipfs://') && !src.startsWith('https://') && !src.startsWith('http://'))
|
|
7
|
+
return src
|
|
8
|
+
const format = src.includes('.gif') ? 'gif' : 'jpeg'
|
|
9
|
+
const params = [`${width}x`, `q${quality}`, format]
|
|
10
|
+
const resolved = src.replace('ipfs://', 'https://ipfs.akita.community/ipfs/')
|
|
11
|
+
return `https://imageproxy.akita.community/${params.join(',')}/${resolved}`
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AlgomdNetwork } from './types'
|
|
2
|
+
|
|
3
|
+
export const NETWORK_DEFAULTS: Record<AlgomdNetwork, {
|
|
4
|
+
algodServer: string
|
|
5
|
+
algodPort?: number
|
|
6
|
+
algodToken?: string
|
|
7
|
+
indexerServer: string
|
|
8
|
+
indexerToken?: string
|
|
9
|
+
}> = {
|
|
10
|
+
mainnet: {
|
|
11
|
+
algodServer: 'https://mainnet-api.4160.nodely.dev',
|
|
12
|
+
indexerServer: 'https://mainnet-idx.4160.nodely.dev',
|
|
13
|
+
},
|
|
14
|
+
testnet: {
|
|
15
|
+
algodServer: 'https://testnet-api.4160.nodely.dev',
|
|
16
|
+
indexerServer: 'https://testnet-idx.4160.nodely.dev',
|
|
17
|
+
},
|
|
18
|
+
localnet: {
|
|
19
|
+
algodServer: 'http://localhost:4001',
|
|
20
|
+
algodToken: 'a'.repeat(64),
|
|
21
|
+
indexerServer: 'http://localhost:8980',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AlgomdNetwork = 'mainnet' | 'testnet' | 'localnet'
|
|
2
|
+
|
|
3
|
+
export interface AlgomdConfig {
|
|
4
|
+
/** Network - determines default algod/indexer URLs */
|
|
5
|
+
network: AlgomdNetwork
|
|
6
|
+
/** Override algod server URL */
|
|
7
|
+
algodServer?: string
|
|
8
|
+
algodPort?: number
|
|
9
|
+
algodToken?: string
|
|
10
|
+
/** Override indexer server URL (needed for Transaction) */
|
|
11
|
+
indexerServer?: string
|
|
12
|
+
indexerToken?: string
|
|
13
|
+
/** Override NFD API URL (default: https://api.nf.domains) */
|
|
14
|
+
nfdApiUrl?: string
|
|
15
|
+
/** Override image URL resolver (default: Akita CDN proxy) */
|
|
16
|
+
resolveImageUrl?: (src: string, width: number, quality?: number) => string
|
|
17
|
+
}
|