diff --git a/.env.sample b/.env.sample index 3f26635..5c80e67 100644 --- a/.env.sample +++ b/.env.sample @@ -27,3 +27,6 @@ INDEXER_TOKEN= INDEXER_SERVER=https://mainnet-idx.algonode.network/ INDEXER_PORT=443 +# Example database (sqlite) +DATABASE_URL=file:./data/data.db + diff --git a/.gitignore b/.gitignore index fd41b0e..d43cce8 100644 --- a/.gitignore +++ b/.gitignore @@ -62,5 +62,9 @@ out/ watermark.txt examples/**/*.json +!examples/**/*.arc32.json __pycache__ + +*.db +*.db-journal diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f8a47cc..5ba1c78 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,7 @@ "mikestead.dotenv", "EditorConfig.EditorConfig", "vitest.explorer", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "Prisma.prisma" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index e1e123e..75d8025 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "files.eol": "\n", + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma" + }, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { diff --git a/README.md b/README.md index 57abd97..66b1148 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,11 @@ subscriber.on('filter1', async (transaction) => { }) //... +// Set up error handling +subscriber.onError((e) => { + // ... +}) + // Either: Start the subscriber (if in long-running process) subscriber.start() @@ -95,8 +100,7 @@ The following code, when algod is pointed to TestNet, will find all transactions The watermark is stored in-memory so this particular example is not resilient to restarts. To change that you can implement proper persistence of the watermark. There is [an example that uses the file system](./examples/data-history-museum/) to demonstrate this. ```typescript -const algod = await algokit.getAlgoClient() -const indexer = await algokit.getAlgoIndexerClient() +const algorand = AlgorandClient.fromEnvironment() let watermark = 0 const subscriber = new AlgorandSubscriber( { @@ -120,14 +124,19 @@ const subscriber = new AlgorandSubscriber( }, }, }, - algod, - indexer, + algorand.client.algod, + algorand.client.indexer, ) subscriber.onBatch('dhm-asset', async (events) => { console.log(`Received ${events.length} asset changes`) // ... do stuff with the events }) +subscriber.onError((e) => { + // eslint-disable-next-line no-console + console.error(e) +}) + subscriber.start() ``` @@ -136,7 +145,7 @@ subscriber.start() The following code, when algod is pointed to MainNet, will find all transfers of [USDC](https://www.circle.com/en/usdc-multichain/algorand) that are greater than $1 and it will poll every 1s for new transfers. ```typescript -const algod = await algokit.getAlgoClient() +const algorand = AlgorandClient.fromEnvironment() let watermark = 0 const subscriber = new AlgorandSubscriber( @@ -160,7 +169,7 @@ const subscriber = new AlgorandSubscriber( }, }, }, - algod, + algorand.client.algod, ) subscriber.on('usdc', (transfer) => { // eslint-disable-next-line no-console @@ -171,6 +180,11 @@ subscriber.on('usdc', (transfer) => { ) }) +subscriber.onError((e) => { + // eslint-disable-next-line no-console + console.error(e) +}) + subscriber.start() ``` diff --git a/docs/README.md b/docs/README.md index fc8139a..7c78675 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,11 @@ subscriber.on('filter1', async (transaction) => { }) //... +// Set up error handling +subscriber.onError((e) => { + // ... +}) + // Either: Start the subscriber (if in long-running process) subscriber.start() diff --git a/docs/code/classes/index.AlgorandSubscriber.md b/docs/code/classes/index.AlgorandSubscriber.md index 55c7b3b..aec9bcb 100644 --- a/docs/code/classes/index.AlgorandSubscriber.md +++ b/docs/code/classes/index.AlgorandSubscriber.md @@ -17,6 +17,7 @@ Handles the logic for subscribing to the Algorand blockchain and emitting events - [abortController](index.AlgorandSubscriber.md#abortcontroller) - [algod](index.AlgorandSubscriber.md#algod) - [config](index.AlgorandSubscriber.md#config) +- [errorEventName](index.AlgorandSubscriber.md#erroreventname) - [eventEmitter](index.AlgorandSubscriber.md#eventemitter) - [filterNames](index.AlgorandSubscriber.md#filternames) - [indexer](index.AlgorandSubscriber.md#indexer) @@ -25,9 +26,11 @@ Handles the logic for subscribing to the Algorand blockchain and emitting events ### Methods +- [defaultErrorHandler](index.AlgorandSubscriber.md#defaulterrorhandler) - [on](index.AlgorandSubscriber.md#on) - [onBatch](index.AlgorandSubscriber.md#onbatch) - [onBeforePoll](index.AlgorandSubscriber.md#onbeforepoll) +- [onError](index.AlgorandSubscriber.md#onerror) - [onPoll](index.AlgorandSubscriber.md#onpoll) - [pollOnce](index.AlgorandSubscriber.md#pollonce) - [start](index.AlgorandSubscriber.md#start) @@ -55,7 +58,7 @@ Create a new `AlgorandSubscriber`. #### Defined in -[subscriber.ts:35](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L35) +[subscriber.ts:41](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L41) ## Properties @@ -65,7 +68,7 @@ Create a new `AlgorandSubscriber`. #### Defined in -[subscriber.ts:23](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L23) +[subscriber.ts:24](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L24) ___ @@ -75,7 +78,7 @@ ___ #### Defined in -[subscriber.ts:20](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L20) +[subscriber.ts:21](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L21) ___ @@ -85,7 +88,17 @@ ___ #### Defined in -[subscriber.ts:22](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L22) +[subscriber.ts:23](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L23) + +___ + +### errorEventName + +• `Private` `Readonly` **errorEventName**: ``"error"`` + +#### Defined in + +[subscriber.ts:30](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L30) ___ @@ -95,7 +108,7 @@ ___ #### Defined in -[subscriber.ts:24](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L24) +[subscriber.ts:25](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L25) ___ @@ -105,7 +118,7 @@ ___ #### Defined in -[subscriber.ts:27](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L27) +[subscriber.ts:28](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L28) ___ @@ -115,7 +128,7 @@ ___ #### Defined in -[subscriber.ts:21](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L21) +[subscriber.ts:22](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L22) ___ @@ -125,7 +138,7 @@ ___ #### Defined in -[subscriber.ts:26](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L26) +[subscriber.ts:27](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L27) ___ @@ -135,10 +148,30 @@ ___ #### Defined in -[subscriber.ts:25](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L25) +[subscriber.ts:26](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L26) ## Methods +### defaultErrorHandler + +▸ **defaultErrorHandler**(`error`): `never` + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `error` | `unknown` | + +#### Returns + +`never` + +#### Defined in + +[subscriber.ts:31](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L31) + +___ + ### on ▸ **on**\<`T`\>(`filterName`, `listener`): [`AlgorandSubscriber`](index.AlgorandSubscriber.md) @@ -181,7 +214,7 @@ new AlgorandSubscriber({filters: [{name: 'my-filter', filter: {...}, mapper: (t) #### Defined in -[subscriber.ts:177](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L177) +[subscriber.ts:191](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L191) ___ @@ -231,7 +264,7 @@ new AlgorandSubscriber({filters: [{name: 'my-filter', filter: {...}, mapper: (t) #### Defined in -[subscriber.ts:203](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L203) +[subscriber.ts:220](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L220) ___ @@ -265,7 +298,58 @@ subscriber.onBeforePoll(async (metadata) => { console.log(metadata.watermark) }) #### Defined in -[subscriber.ts:221](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L221) +[subscriber.ts:238](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L238) + +___ + +### onError + +▸ **onError**(`listener`): [`AlgorandSubscriber`](index.AlgorandSubscriber.md) + +Register an error handler to run if an error is thrown during processing or event handling. + +This is useful to handle any errors that occur and can be used to perform retries, logging or cleanup activities. + +The listener can be async and it will be awaited if so. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `listener` | [`ErrorListener`](../modules/types_subscription.md#errorlistener) | The listener function to invoke with the error that was thrown | + +#### Returns + +[`AlgorandSubscriber`](index.AlgorandSubscriber.md) + +The subscriber so `on*` calls can be chained + +**`Example`** + +```typescript +subscriber.onError((error) => { console.error(error) }) +``` + +**`Example`** + +```typescript +const maxRetries = 3 +let retryCount = 0 +subscriber.onError(async (error) => { + retryCount++ + if (retryCount > maxRetries) { + console.error(error) + return + } + console.log(`Error occurred, retrying in 2 seconds (${retryCount}/${maxRetries})`) + await new Promise((r) => setTimeout(r, 2_000)) + subscriber.start() +}) +``` + +#### Defined in + +[subscriber.ts:292](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L292) ___ @@ -302,7 +386,7 @@ subscriber.onPoll(async (pollResult) => { console.log(pollResult.subscribedTrans #### Defined in -[subscriber.ts:242](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L242) +[subscriber.ts:259](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L259) ___ @@ -323,7 +407,7 @@ The poll result #### Defined in -[subscriber.ts:61](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L61) +[subscriber.ts:67](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L67) ___ @@ -350,7 +434,7 @@ An object that contains a promise you can wait for after calling stop #### Defined in -[subscriber.ts:107](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L107) +[subscriber.ts:113](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L113) ___ @@ -374,4 +458,4 @@ A promise that can be awaited to ensure the subscriber has finished stopping #### Defined in -[subscriber.ts:150](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L150) +[subscriber.ts:164](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/subscriber.ts#L164) diff --git a/docs/code/enums/types_subscription.BalanceChangeRole.md b/docs/code/enums/types_subscription.BalanceChangeRole.md index 18afa68..2c11e21 100644 --- a/docs/code/enums/types_subscription.BalanceChangeRole.md +++ b/docs/code/enums/types_subscription.BalanceChangeRole.md @@ -26,7 +26,7 @@ Account was creating an asset and holds the full asset supply #### Defined in -[types/subscription.ts:93](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L93) +[types/subscription.ts:95](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L95) ___ @@ -39,7 +39,7 @@ A balance change with this role will always have a 0 amount and use the asset ma #### Defined in -[types/subscription.ts:97](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L97) +[types/subscription.ts:99](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L99) ___ @@ -51,7 +51,7 @@ Account was having an asset amount closed to it #### Defined in -[types/subscription.ts:91](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L91) +[types/subscription.ts:93](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L93) ___ @@ -63,7 +63,7 @@ Account was receiving a transaction #### Defined in -[types/subscription.ts:89](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L89) +[types/subscription.ts:91](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L91) ___ @@ -75,4 +75,4 @@ Account was sending a transaction (sending asset and/or spending fee if asset `0 #### Defined in -[types/subscription.ts:87](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L87) +[types/subscription.ts:89](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L89) diff --git a/docs/code/interfaces/types_subscription.AlgorandSubscriberConfig.md b/docs/code/interfaces/types_subscription.AlgorandSubscriberConfig.md index a90433a..aa833b3 100644 --- a/docs/code/interfaces/types_subscription.AlgorandSubscriberConfig.md +++ b/docs/code/interfaces/types_subscription.AlgorandSubscriberConfig.md @@ -39,7 +39,7 @@ Any ARC-28 event definitions to process from app call logs #### Defined in -[types/subscription.ts:133](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L133) +[types/subscription.ts:135](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L135) ___ @@ -55,7 +55,7 @@ The set of filters to subscribe to / emit events for, along with optional data m #### Defined in -[types/subscription.ts:261](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L261) +[types/subscription.ts:263](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L263) ___ @@ -67,7 +67,7 @@ The frequency to poll for new blocks in seconds; defaults to 1s #### Defined in -[types/subscription.ts:263](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L263) +[types/subscription.ts:265](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L265) ___ @@ -90,7 +90,7 @@ boundary based on the number of rounds specified here. #### Defined in -[types/subscription.ts:153](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L153) +[types/subscription.ts:155](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L155) ___ @@ -112,7 +112,7 @@ your catchup speed when using `sync-oldest`. #### Defined in -[types/subscription.ts:142](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L142) +[types/subscription.ts:144](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L144) ___ @@ -143,7 +143,7 @@ past `watermark` then how should that be handled: #### Defined in -[types/subscription.ts:171](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L171) +[types/subscription.ts:173](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L173) ___ @@ -155,7 +155,7 @@ Whether to wait via algod `/status/wait-for-block-after` endpoint when at the ti #### Defined in -[types/subscription.ts:265](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L265) +[types/subscription.ts:267](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L267) ___ @@ -175,4 +175,4 @@ its position in the chain #### Defined in -[types/subscription.ts:268](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L268) +[types/subscription.ts:270](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L270) diff --git a/docs/code/interfaces/types_subscription.BalanceChange.md b/docs/code/interfaces/types_subscription.BalanceChange.md index 1b8b3b4..aed8472 100644 --- a/docs/code/interfaces/types_subscription.BalanceChange.md +++ b/docs/code/interfaces/types_subscription.BalanceChange.md @@ -25,7 +25,7 @@ The address that the balance change is for. #### Defined in -[types/subscription.ts:75](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L75) +[types/subscription.ts:77](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L77) ___ @@ -37,7 +37,7 @@ The amount of the balance change in smallest divisible unit or microAlgos. #### Defined in -[types/subscription.ts:79](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L79) +[types/subscription.ts:81](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L81) ___ @@ -49,7 +49,7 @@ The asset ID of the balance change, or 0 for Algos. #### Defined in -[types/subscription.ts:77](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L77) +[types/subscription.ts:79](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L79) ___ @@ -61,4 +61,4 @@ The roles the account was playing that led to the balance change #### Defined in -[types/subscription.ts:81](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L81) +[types/subscription.ts:83](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L83) diff --git a/docs/code/interfaces/types_subscription.BeforePollMetadata.md b/docs/code/interfaces/types_subscription.BeforePollMetadata.md index 8c091f0..2564c34 100644 --- a/docs/code/interfaces/types_subscription.BeforePollMetadata.md +++ b/docs/code/interfaces/types_subscription.BeforePollMetadata.md @@ -23,7 +23,7 @@ The current round of algod #### Defined in -[types/subscription.ts:105](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L105) +[types/subscription.ts:107](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L107) ___ @@ -35,4 +35,4 @@ The current watermark of the subscriber #### Defined in -[types/subscription.ts:103](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L103) +[types/subscription.ts:105](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L105) diff --git a/docs/code/interfaces/types_subscription.BlockMetadata.md b/docs/code/interfaces/types_subscription.BlockMetadata.md index 75ff0f6..0cddb4c 100644 --- a/docs/code/interfaces/types_subscription.BlockMetadata.md +++ b/docs/code/interfaces/types_subscription.BlockMetadata.md @@ -30,7 +30,7 @@ Full count of transactions and inner transactions (recursively) in this block. #### Defined in -[types/subscription.ts:48](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L48) +[types/subscription.ts:50](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L50) ___ @@ -42,7 +42,7 @@ The base64 genesis hash of the chain. #### Defined in -[types/subscription.ts:40](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L40) +[types/subscription.ts:42](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L42) ___ @@ -54,7 +54,7 @@ The genesis ID of the chain. #### Defined in -[types/subscription.ts:38](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L38) +[types/subscription.ts:40](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L40) ___ @@ -64,7 +64,7 @@ ___ #### Defined in -[types/subscription.ts:32](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L32) +[types/subscription.ts:34](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L34) ___ @@ -76,7 +76,7 @@ Count of parent transactions in this block #### Defined in -[types/subscription.ts:46](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L46) +[types/subscription.ts:48](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L48) ___ @@ -88,7 +88,7 @@ The previous block hash. #### Defined in -[types/subscription.ts:42](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L42) +[types/subscription.ts:44](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L44) ___ @@ -100,7 +100,7 @@ The round of the block. #### Defined in -[types/subscription.ts:34](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L34) +[types/subscription.ts:36](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L36) ___ @@ -112,7 +112,7 @@ The base64 seed of the block. #### Defined in -[types/subscription.ts:44](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L44) +[types/subscription.ts:46](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L46) ___ @@ -124,4 +124,4 @@ The ISO 8601 timestamp of the block. #### Defined in -[types/subscription.ts:36](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L36) +[types/subscription.ts:38](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L38) diff --git a/docs/code/interfaces/types_subscription.CoreTransactionSubscriptionParams.md b/docs/code/interfaces/types_subscription.CoreTransactionSubscriptionParams.md index 41356a0..7c9bd57 100644 --- a/docs/code/interfaces/types_subscription.CoreTransactionSubscriptionParams.md +++ b/docs/code/interfaces/types_subscription.CoreTransactionSubscriptionParams.md @@ -34,7 +34,7 @@ Any ARC-28 event definitions to process from app call logs #### Defined in -[types/subscription.ts:133](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L133) +[types/subscription.ts:135](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L135) ___ @@ -65,7 +65,7 @@ A list of filters with corresponding names. #### Defined in -[types/subscription.ts:131](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L131) +[types/subscription.ts:133](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L133) ___ @@ -84,7 +84,7 @@ boundary based on the number of rounds specified here. #### Defined in -[types/subscription.ts:153](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L153) +[types/subscription.ts:155](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L155) ___ @@ -102,7 +102,7 @@ your catchup speed when using `sync-oldest`. #### Defined in -[types/subscription.ts:142](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L142) +[types/subscription.ts:144](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L144) ___ @@ -129,4 +129,4 @@ past `watermark` then how should that be handled: #### Defined in -[types/subscription.ts:171](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L171) +[types/subscription.ts:173](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L173) diff --git a/docs/code/interfaces/types_subscription.NamedTransactionFilter.md b/docs/code/interfaces/types_subscription.NamedTransactionFilter.md index 51d2984..efe5fc3 100644 --- a/docs/code/interfaces/types_subscription.NamedTransactionFilter.md +++ b/docs/code/interfaces/types_subscription.NamedTransactionFilter.md @@ -29,7 +29,7 @@ The filter itself. #### Defined in -[types/subscription.ts:179](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L179) +[types/subscription.ts:181](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L181) ___ @@ -41,4 +41,4 @@ The name to give the filter. #### Defined in -[types/subscription.ts:177](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L177) +[types/subscription.ts:179](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L179) diff --git a/docs/code/interfaces/types_subscription.SubscriberConfigFilter.md b/docs/code/interfaces/types_subscription.SubscriberConfigFilter.md index 4cb21f3..657c05f 100644 --- a/docs/code/interfaces/types_subscription.SubscriberConfigFilter.md +++ b/docs/code/interfaces/types_subscription.SubscriberConfigFilter.md @@ -40,7 +40,7 @@ The filter itself. #### Defined in -[types/subscription.ts:179](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L179) +[types/subscription.ts:181](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L181) ___ @@ -70,7 +70,7 @@ Note: if you provide multiple filters with the same name then only the mapper of #### Defined in -[types/subscription.ts:284](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L284) +[types/subscription.ts:286](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L286) ___ @@ -86,4 +86,4 @@ The name to give the filter. #### Defined in -[types/subscription.ts:177](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L177) +[types/subscription.ts:179](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L179) diff --git a/docs/code/interfaces/types_subscription.TransactionFilter.md b/docs/code/interfaces/types_subscription.TransactionFilter.md index 5c83cfe..46ea3af 100644 --- a/docs/code/interfaces/types_subscription.TransactionFilter.md +++ b/docs/code/interfaces/types_subscription.TransactionFilter.md @@ -51,7 +51,7 @@ Filter to app transactions that meet the given app arguments predicate. #### Defined in -[types/subscription.ts:212](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L212) +[types/subscription.ts:214](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L214) ___ @@ -63,7 +63,7 @@ Filter to transactions that are creating an app. #### Defined in -[types/subscription.ts:195](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L195) +[types/subscription.ts:197](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L197) ___ @@ -75,7 +75,7 @@ Filter to transactions against the app with the given ID(s). #### Defined in -[types/subscription.ts:193](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L193) +[types/subscription.ts:195](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L195) ___ @@ -87,7 +87,7 @@ Filter to transactions that have given on complete(s). #### Defined in -[types/subscription.ts:197](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L197) +[types/subscription.ts:199](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L199) ___ @@ -100,7 +100,7 @@ Note: the definitions for these events must be passed in to the subscription con #### Defined in -[types/subscription.ts:216](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L216) +[types/subscription.ts:218](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L218) ___ @@ -112,7 +112,7 @@ Filter to transactions that are creating an asset. #### Defined in -[types/subscription.ts:201](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L201) +[types/subscription.ts:203](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L203) ___ @@ -124,7 +124,7 @@ Filter to transactions against the asset with the given ID(s). #### Defined in -[types/subscription.ts:199](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L199) +[types/subscription.ts:201](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L201) ___ @@ -136,7 +136,7 @@ Filter to transactions that result in balance changes that match one or more of #### Defined in -[types/subscription.ts:218](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L218) +[types/subscription.ts:220](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L220) ___ @@ -162,7 +162,7 @@ Catch-all custom filter to filter for things that the rest of the filters don't #### Defined in -[types/subscription.ts:235](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L235) +[types/subscription.ts:237](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L237) ___ @@ -175,7 +175,7 @@ or equal to the given maximum (microAlgos or decimal units of an ASA if type: ax #### Defined in -[types/subscription.ts:207](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L207) +[types/subscription.ts:209](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L209) ___ @@ -188,7 +188,7 @@ the given method signature as the first app argument. #### Defined in -[types/subscription.ts:210](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L210) +[types/subscription.ts:212](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L212) ___ @@ -201,7 +201,7 @@ than or equal to the given minimum (microAlgos or decimal units of an ASA if typ #### Defined in -[types/subscription.ts:204](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L204) +[types/subscription.ts:206](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L206) ___ @@ -213,7 +213,7 @@ Filter to transactions with a note having the given prefix. #### Defined in -[types/subscription.ts:191](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L191) +[types/subscription.ts:193](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L193) ___ @@ -225,7 +225,7 @@ Filter to transactions being received by the specified address(es). #### Defined in -[types/subscription.ts:189](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L189) +[types/subscription.ts:191](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L191) ___ @@ -237,7 +237,7 @@ Filter to transactions sent from the specified address(es). #### Defined in -[types/subscription.ts:187](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L187) +[types/subscription.ts:189](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L189) ___ @@ -249,4 +249,4 @@ Filter based on the given transaction type(s). #### Defined in -[types/subscription.ts:185](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L185) +[types/subscription.ts:187](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L187) diff --git a/docs/code/interfaces/types_subscription.TransactionSubscriptionParams.md b/docs/code/interfaces/types_subscription.TransactionSubscriptionParams.md index 1f4d49a..ec3c1cc 100644 --- a/docs/code/interfaces/types_subscription.TransactionSubscriptionParams.md +++ b/docs/code/interfaces/types_subscription.TransactionSubscriptionParams.md @@ -38,7 +38,7 @@ Any ARC-28 event definitions to process from app call logs #### Defined in -[types/subscription.ts:133](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L133) +[types/subscription.ts:135](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L135) ___ @@ -51,7 +51,7 @@ If not provided, it will be resolved on demand. #### Defined in -[types/subscription.ts:255](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L255) +[types/subscription.ts:257](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L257) ___ @@ -86,7 +86,7 @@ A list of filters with corresponding names. #### Defined in -[types/subscription.ts:131](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L131) +[types/subscription.ts:133](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L133) ___ @@ -109,7 +109,7 @@ boundary based on the number of rounds specified here. #### Defined in -[types/subscription.ts:153](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L153) +[types/subscription.ts:155](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L155) ___ @@ -131,7 +131,7 @@ your catchup speed when using `sync-oldest`. #### Defined in -[types/subscription.ts:142](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L142) +[types/subscription.ts:144](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L144) ___ @@ -162,7 +162,7 @@ past `watermark` then how should that be handled: #### Defined in -[types/subscription.ts:171](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L171) +[types/subscription.ts:173](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L173) ___ @@ -182,4 +182,4 @@ will be slow if `onMaxRounds` is `sync-oldest`. #### Defined in -[types/subscription.ts:250](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L250) +[types/subscription.ts:252](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L252) diff --git a/docs/code/interfaces/types_subscription.TransactionSubscriptionResult.md b/docs/code/interfaces/types_subscription.TransactionSubscriptionResult.md index 26f59a0..2282317 100644 --- a/docs/code/interfaces/types_subscription.TransactionSubscriptionResult.md +++ b/docs/code/interfaces/types_subscription.TransactionSubscriptionResult.md @@ -13,6 +13,7 @@ The result of a single subscription pull/poll. - [blockMetadata](types_subscription.TransactionSubscriptionResult.md#blockmetadata) - [currentRound](types_subscription.TransactionSubscriptionResult.md#currentround) - [newWatermark](types_subscription.TransactionSubscriptionResult.md#newwatermark) +- [startingWatermark](types_subscription.TransactionSubscriptionResult.md#startingwatermark) - [subscribedTransactions](types_subscription.TransactionSubscriptionResult.md#subscribedtransactions) - [syncedRoundRange](types_subscription.TransactionSubscriptionResult.md#syncedroundrange) @@ -27,7 +28,7 @@ of the subscription poll. #### Defined in -[types/subscription.ts:27](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L27) +[types/subscription.ts:29](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L29) ___ @@ -55,7 +56,19 @@ subscribed transactions to keep it reliable. #### Defined in -[types/subscription.ts:17](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L17) +[types/subscription.ts:19](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L19) + +___ + +### startingWatermark + +• **startingWatermark**: `number` + +The watermark value that was retrieved at the start of the subscription poll. + +#### Defined in + +[types/subscription.ts:13](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L13) ___ @@ -70,7 +83,7 @@ to represent the data with some additional fields. #### Defined in -[types/subscription.ts:23](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L23) +[types/subscription.ts:25](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L25) ___ diff --git a/docs/code/modules/types.md b/docs/code/modules/types.md index 0e47af9..569bf28 100644 --- a/docs/code/modules/types.md +++ b/docs/code/modules/types.md @@ -26,6 +26,7 @@ - [BlockVote](types.md#blockvote) - [CoreTransactionSubscriptionParams](types.md#coretransactionsubscriptionparams) - [EmittedArc28Event](types.md#emittedarc28event) +- [ErrorListener](types.md#errorlistener) - [LogicSig](types.md#logicsig) - [MultisigSig](types.md#multisigsig) - [NamedTransactionFilter](types.md#namedtransactionfilter) @@ -161,6 +162,12 @@ Re-exports [EmittedArc28Event](../interfaces/types_arc_28.EmittedArc28Event.md) ___ +### ErrorListener + +Re-exports [ErrorListener](types_subscription.md#errorlistener) + +___ + ### LogicSig Re-exports [LogicSig](../interfaces/types_block.LogicSig.md) diff --git a/docs/code/modules/types_subscription.md b/docs/code/modules/types_subscription.md index 61a350d..3a051fa 100644 --- a/docs/code/modules/types_subscription.md +++ b/docs/code/modules/types_subscription.md @@ -23,11 +23,36 @@ ### Type Aliases +- [ErrorListener](types_subscription.md#errorlistener) - [SubscribedTransaction](types_subscription.md#subscribedtransaction) - [TypedAsyncEventListener](types_subscription.md#typedasynceventlistener) ## Type Aliases +### ErrorListener + +Ƭ **ErrorListener**: (`error`: `unknown`) => `Promise`\<`void`\> \| `void` + +#### Type declaration + +▸ (`error`): `Promise`\<`void`\> \| `void` + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `error` | `unknown` | + +##### Returns + +`Promise`\<`void`\> \| `void` + +#### Defined in + +[types/subscription.ts:291](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L291) + +___ + ### SubscribedTransaction Ƭ **SubscribedTransaction**: `TransactionResult` & \{ `arc28Events?`: [`EmittedArc28Event`](../interfaces/types_arc_28.EmittedArc28Event.md)[] ; `balanceChanges?`: [`BalanceChange`](../interfaces/types_subscription.BalanceChange.md)[] ; `filtersMatched?`: `string`[] ; `inner-txns?`: [`SubscribedTransaction`](types_subscription.md#subscribedtransaction)[] ; `parentTransactionId?`: `string` } @@ -42,7 +67,7 @@ Substantively, based on the Indexer [`TransactionResult` model](https://develop #### Defined in -[types/subscription.ts:59](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L59) +[types/subscription.ts:61](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L61) ___ @@ -73,4 +98,4 @@ ___ #### Defined in -[types/subscription.ts:287](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L287) +[types/subscription.ts:289](https://github.com/algorandfoundation/algokit-subscriber-ts/blob/main/src/types/subscription.ts#L289) diff --git a/docs/subscriber.md b/docs/subscriber.md index 8caaa9b..cb6128c 100644 --- a/docs/subscriber.md +++ b/docs/subscriber.md @@ -247,6 +247,56 @@ If you use `start` then you can stop the polling by calling `stop`, which can be ) ``` +## Handling errors + +Because `start` isn't a blocking method, you can't simply wrap it in a try/catch. +To handle errors, you can register error handlers/listeners using the `onError` method. This works in a similar way to the other `on*` methods. + +````typescript +/** + * Register an error handler to run if an error is thrown during processing or event handling. + * + * This is useful to handle any errors that occur and can be used to perform retries, logging or cleanup activities. + * + * The listener can be async and it will be awaited if so. + * @example + * ```typescript + * subscriber.onError((error) => { console.error(error) }) + * ``` + * @example + * ```typescript + * const maxRetries = 3 + * let retryCount = 0 + * subscriber.onError(async (error) => { + * retryCount++ + * if (retryCount > maxRetries) { + * console.error(error) + * return + * } + * console.log(`Error occurred, retrying in 2 seconds (${retryCount}/${maxRetries})`) + * await new Promise((r) => setTimeout(r, 2_000)) + * subscriber.start() + *}) + * ``` + * @param listener The listener function to invoke with the error that was thrown + * @returns The subscriber so `on*` calls can be chained + */ + onError(listener: ErrorListener) {} +```` + +The `ErrorListener` type is defined as: + +```typescript +type ErrorListener = (error: unknown) => Promise | void +``` + +This allows you to use async or sync error listeners. + +Multiple error listeners can be added, and each will be called one-by-one (and awaited) in the order the registrations occur. + +When no error listeners have been registered, a default listener is used to re-throw any exception, so they can be caught by global uncaught exception handlers. +Once an error listener has been registered, the default listener is removed and it's the responsibility of the registered error listener to perform any error handling. + ## Examples See the [main README](../README.md#examples). diff --git a/docs/subscriptions.md b/docs/subscriptions.md index 4e5e7d2..f2baf93 100644 --- a/docs/subscriptions.md +++ b/docs/subscriptions.md @@ -211,6 +211,8 @@ export interface TransactionSubscriptionResult { syncedRoundRange: [startRound: number, endRound: number] /** The current detected tip of the configured Algorand blockchain. */ currentRound: number + /** The watermark value that was retrieved at the start of the subscription poll. */ + startingWatermark: number /** The new watermark value to persist for the next call to * `getSubscribedTransactions` to continue the sync. * Will be equal to `syncedRoundRange[1]`. Only persist this diff --git a/examples/data-history-museum/index.ts b/examples/data-history-museum/index.ts index 12d7db5..85a6312 100644 --- a/examples/data-history-museum/index.ts +++ b/examples/data-history-museum/index.ts @@ -129,12 +129,26 @@ async function saveTransactions(transactions: unknown[], fileName: string) { console.log(`Saved ${transactions.length} transactions to ${fileName}`) } -// eslint-disable-next-line no-console -process.on('uncaughtException', (e) => console.error(e)) ;(async () => { const subscriber = await getDHMSubscriber() if (process.env.RUN_LOOP === 'true') { + // Restart on error + const maxRetries = 3 + let retryCount = 0 + subscriber.onError(async (e) => { + retryCount++ + if (retryCount > maxRetries) { + // eslint-disable-next-line no-console + console.error(e) + return + } + // eslint-disable-next-line no-console + console.log(`Error occurred, retrying in 2 seconds (${retryCount}/${maxRetries})`) + await new Promise((r) => setTimeout(r, 2_000)) + subscriber.start() + }) + subscriber.start() ;['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, () => { diff --git a/examples/usdc/index.ts b/examples/usdc/index.ts index 8a4889e..fe20978 100644 --- a/examples/usdc/index.ts +++ b/examples/usdc/index.ts @@ -11,8 +11,6 @@ if (!fs.existsSync(path.join(__dirname, '..', '..', '.env')) && !process.env.ALG process.exit(1) } -// eslint-disable-next-line no-console -process.on('uncaughtException', (e) => console.error(e)) ;(async () => { const algod = await algokit.getAlgoClient() let watermark = 0 @@ -48,7 +46,10 @@ process.on('uncaughtException', (e) => console.error(e)) ).toFixed(2)} in transaction ${transfer.id}`, ) }) - + subscriber.onError((e) => { + // eslint-disable-next-line no-console + console.error(e) + }) subscriber.start() ;['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, () => { diff --git a/examples/xgov-voting/index.ts b/examples/xgov-voting/index.ts index 0e8e788..f8936cf 100644 --- a/examples/xgov-voting/index.ts +++ b/examples/xgov-voting/index.ts @@ -1,8 +1,11 @@ -import * as algokit from '@algorandfoundation/algokit-utils' -import algosdk from 'algosdk' +import { AlgorandClient } from '@algorandfoundation/algokit-utils' +import { Prisma, PrismaClient } from '@prisma/client' +import algosdk, { ABIValue } from 'algosdk' import fs from 'fs' import path from 'path' import { AlgorandSubscriber } from '../../src/subscriber' +import { VotingRoundAppClient } from './types/voting-app-client' +import { VoteType, VotingRoundMetadata } from './types/voting-round' import ABIArrayDynamicType = algosdk.ABIArrayDynamicType import ABIUintType = algosdk.ABIUintType import TransactionType = algosdk.TransactionType @@ -13,9 +16,66 @@ if (!fs.existsSync(path.join(__dirname, '..', '..', '.env')) && !process.env.ALG process.exit(1) } +const prisma = new PrismaClient() + +const votingRoundId = 1821334702 // Grab from https://voting.algorand.foundation/ +const watermarkId = `voting-${votingRoundId}` + async function getXGovSubscriber() { - const algod = await algokit.getAlgoClient() - const indexer = await algokit.getAlgoIndexerClient() + const algorand = AlgorandClient.fromEnvironment() + + // Get voting round metadata + const appClient = algorand.client.getTypedAppClientById(VotingRoundAppClient, { + id: votingRoundId, + }) + const votingRoundState = await appClient.getGlobalState() + const votingRoundMetadata = (await ( + await fetch(`https://ipfs.algonode.xyz/ipfs/${votingRoundState.metadataIpfsCid!.asString()}`) + ).json()) as VotingRoundMetadata + + const answerIndexMetadata = votingRoundMetadata.questions.flatMap((q) => + q.options.map((o, i) => ({ + questionId: q.id, + optionIndex: i, + optionId: o.id, + })), + ) + + const useWeighting = votingRoundMetadata.type === VoteType.WEIGHTING || votingRoundMetadata.type === VoteType.PARTITIONED_WEIGHTING + const answerAppArgsIndex = useWeighting ? 4 : 3 + const answerArrayType = new ABIArrayDynamicType(new ABIUintType(useWeighting ? 64 : 8)) + + // Insert metadata into sqlite (one-shot create since it's idempotent data) + if (!(await prisma.votingRound.findFirst({ where: { id: votingRoundId.toString() } }))) { + await prisma.$transaction(async (p) => { + const result = await p.votingRound.create({ + data: { + id: votingRoundId.toString(), + title: votingRoundMetadata.title, + questions: { + createMany: { + data: votingRoundMetadata.questions.map((q) => ({ + id: q.id, + prompt: q.prompt, + })), + }, + }, + }, + }) + const result2 = await p.votingRoundQuestionOption.createMany({ + data: votingRoundMetadata.questions.flatMap((q) => + q.options.map((o, i) => ({ + id: o.id, + questionId: q.id, + optionIndex: i, + prompt: o.label, + })), + ), + }) + console.log('Created voting round metadata', result, result2) + }) + } + const subscriber = new AlgorandSubscriber( { filters: [ @@ -23,26 +83,118 @@ async function getXGovSubscriber() { name: 'xgov-vote', filter: { type: TransactionType.appl, - appId: 1236654302, // MainNet: xGov Voting Session 2 + appId: votingRoundId, methodSignature: 'vote(pay,byte[],uint64,uint8[],uint64[],application)void', }, }, ], - frequencyInSeconds: 30, + frequencyInSeconds: 5, maxRoundsToSync: 100, syncBehaviour: 'catchup-with-indexer', watermarkPersistence: { - get: getLastWatermark, - set: saveWatermark, + get: async () => (await prisma.watermark.findUnique({ where: { id: watermarkId }, select: { watermark: true } }))?.watermark ?? 0, + set: async (_watermark) => { + /* Happens in onPoll() */ + }, }, }, - algod, - indexer, + algorand.client.algod, + algorand.client.indexer, ) + subscriber.onPoll(async (poll) => { + const result = await prisma.$transaction( + async (p) => { + // Optimistic locking of watermark from current poll + const expectedStartingWatermark = + (await p.watermark.findUnique({ where: { id: watermarkId }, select: { watermark: true } }))?.watermark ?? 0 + if (expectedStartingWatermark !== poll.startingWatermark) { + throw new Error(`Watermark mismatch; expected ${expectedStartingWatermark} but got ${poll.startingWatermark}`) + } + + const updateWatermark = async () => { + return await p.watermark.upsert({ + create: { + id: watermarkId, + watermark: poll.newWatermark, + updated: new Date().toISOString(), + }, + update: { + watermark: poll.newWatermark, + updated: new Date().toISOString(), + }, + where: { + id: watermarkId, + }, + }) + } + + if (poll.subscribedTransactions.length === 0) { + const watermark = await updateWatermark() + console.log(`No new transactions found in rounds ${poll.syncedRoundRange[0]}-${poll.syncedRoundRange[1]}`, watermark) + return + } + + const votes = await p.vote.createMany({ + data: poll.subscribedTransactions.map((t) => ({ + id: t.id, + voterAddress: t.sender, + votingRoundId: votingRoundId.toString(), + castedAt: new Date(t['round-time']! * 1000).toISOString(), + })), + }) + + const casts = await p.voteCast.createMany({ + data: poll.subscribedTransactions.flatMap((t) => { + return answerArrayType + .decode(Buffer.from(t!['application-transaction']!['application-args']![answerAppArgsIndex], 'base64')) + .map((v: ABIValue, i: number) => { + if (!useWeighting) { + const questionIndex = i + const answerIndex = parseInt(v.toString()) + const questionOptionIndex = answerIndexMetadata.findIndex( + (a) => a.questionId === votingRoundMetadata.questions[questionIndex].id && a.optionIndex === answerIndex, + ) + return { + id: `${t.id}-${answerIndexMetadata[questionOptionIndex].optionId}`, + questionOptionId: answerIndexMetadata[questionOptionIndex].optionId, + optionIndex: answerIndexMetadata[questionOptionIndex].optionIndex, + voteId: t.id, + voteWeight: '1', + } + } + return { + id: `${t.id}-${answerIndexMetadata[i].optionId}`, + questionOptionId: answerIndexMetadata[i].optionId, + optionIndex: answerIndexMetadata[i].optionIndex, + voteId: t.id, + voteWeight: v.toString(), + } + }) + }), + }) + + const watermark = await updateWatermark() + + console.log( + `Finished persisting ${poll.subscribedTransactions.length} matched transactions from rounds ${poll.syncedRoundRange[0]}-${poll.syncedRoundRange[1]} to database`, + 'votes', + votes, + 'casts', + casts, + 'watermark', + watermark, + ) + }, + { + isolationLevel: Prisma.TransactionIsolationLevel.Serializable, + timeout: 30_000, + }, + ) + }) + // eslint-disable-next-line no-console subscriber.on('xgov-vote', (event) => { - const abiUint64Array = new ABIArrayDynamicType(new ABIUintType(64)) - const votes = abiUint64Array.decode(Buffer.from(event!['application-transaction']!['application-args']![4], 'base64')) + const votes = answerArrayType.decode(Buffer.from(event!['application-transaction']!['application-args']![answerAppArgsIndex], 'base64')) // eslint-disable-next-line no-console console.log(`${event.sender} voted with txn ${event.id} with votes:`, votes) }) @@ -63,12 +215,14 @@ async function getLastWatermark(): Promise { return Number(existing) } -// eslint-disable-next-line no-console -process.on('uncaughtException', (e) => console.error(e)) ;(async () => { const subscriber = await getXGovSubscriber() if (process.env.RUN_LOOP === 'true') { + subscriber.onError((e) => { + // eslint-disable-next-line no-console + console.error(e) + }) subscriber.start() ;['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, () => { @@ -80,7 +234,9 @@ process.on('uncaughtException', (e) => console.error(e)) } else { await subscriber.pollOnce() } + prisma.$disconnect() })().catch((e) => { // eslint-disable-next-line no-console console.error(e) + prisma.$disconnect() }) diff --git a/examples/xgov-voting/prisma/migrations/20240619152002_init/migration.sql b/examples/xgov-voting/prisma/migrations/20240619152002_init/migration.sql new file mode 100644 index 0000000..29f234e --- /dev/null +++ b/examples/xgov-voting/prisma/migrations/20240619152002_init/migration.sql @@ -0,0 +1,51 @@ +-- CreateTable +CREATE TABLE "VotingRound" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "VotingRoundQuestion" ( + "id" TEXT NOT NULL PRIMARY KEY, + "votingRoundId" TEXT NOT NULL, + "prompt" TEXT NOT NULL, + CONSTRAINT "VotingRoundQuestion_votingRoundId_fkey" FOREIGN KEY ("votingRoundId") REFERENCES "VotingRound" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "VotingRoundQuestionOption" ( + "id" TEXT NOT NULL PRIMARY KEY, + "questionId" TEXT NOT NULL, + "optionIndex" INTEGER NOT NULL, + "prompt" TEXT NOT NULL, + CONSTRAINT "VotingRoundQuestionOption_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "VotingRoundQuestion" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Vote" ( + "id" TEXT NOT NULL PRIMARY KEY, + "castedAt" TEXT NOT NULL, + "voterAddress" TEXT NOT NULL, + "votingRoundId" TEXT NOT NULL, + CONSTRAINT "Vote_votingRoundId_fkey" FOREIGN KEY ("votingRoundId") REFERENCES "VotingRound" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "VoteCast" ( + "id" TEXT NOT NULL PRIMARY KEY, + "voteId" TEXT NOT NULL, + "questionOptionId" TEXT NOT NULL, + "optionIndex" INTEGER NOT NULL, + "voteWeight" TEXT NOT NULL, + "votingRoundQuestionId" TEXT, + CONSTRAINT "VoteCast_voteId_fkey" FOREIGN KEY ("voteId") REFERENCES "Vote" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "VoteCast_questionOptionId_fkey" FOREIGN KEY ("questionOptionId") REFERENCES "VotingRoundQuestionOption" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "VoteCast_votingRoundQuestionId_fkey" FOREIGN KEY ("votingRoundQuestionId") REFERENCES "VotingRoundQuestion" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Watermark" ( + "id" TEXT NOT NULL PRIMARY KEY, + "watermark" INTEGER NOT NULL, + "updated" TEXT NOT NULL +); diff --git a/examples/xgov-voting/prisma/migrations/20240619153715_tweak/migration.sql b/examples/xgov-voting/prisma/migrations/20240619153715_tweak/migration.sql new file mode 100644 index 0000000..be9c734 --- /dev/null +++ b/examples/xgov-voting/prisma/migrations/20240619153715_tweak/migration.sql @@ -0,0 +1,23 @@ +/* + Warnings: + + - You are about to drop the column `votingRoundQuestionId` on the `VoteCast` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_VoteCast" ( + "id" TEXT NOT NULL PRIMARY KEY, + "voteId" TEXT NOT NULL, + "questionOptionId" TEXT NOT NULL, + "optionIndex" INTEGER NOT NULL, + "voteWeight" TEXT NOT NULL, + CONSTRAINT "VoteCast_voteId_fkey" FOREIGN KEY ("voteId") REFERENCES "Vote" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "VoteCast_questionOptionId_fkey" FOREIGN KEY ("questionOptionId") REFERENCES "VotingRoundQuestionOption" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_VoteCast" ("id", "optionIndex", "questionOptionId", "voteId", "voteWeight") SELECT "id", "optionIndex", "questionOptionId", "voteId", "voteWeight" FROM "VoteCast"; +DROP TABLE "VoteCast"; +ALTER TABLE "new_VoteCast" RENAME TO "VoteCast"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/examples/xgov-voting/prisma/migrations/migration_lock.toml b/examples/xgov-voting/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/examples/xgov-voting/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/examples/xgov-voting/prisma/schema.prisma b/examples/xgov-voting/prisma/schema.prisma new file mode 100644 index 0000000..d37de78 --- /dev/null +++ b/examples/xgov-voting/prisma/schema.prisma @@ -0,0 +1,60 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model VotingRound { + // App ID of the voting round contract instance + id String @id + title String + questions VotingRoundQuestion[] + votes Vote[] +} + +model VotingRoundQuestion { + id String @id + votingRoundId String + votingRound VotingRound @relation(fields: [votingRoundId], references: [id]) + prompt String + options VotingRoundQuestionOption[] +} + +model VotingRoundQuestionOption { + id String @id + questionId String + question VotingRoundQuestion @relation(fields: [questionId], references: [id]) + optionIndex Int + prompt String + VoteCast VoteCast[] +} + +model Vote { + // Transaction ID of the vote + id String @id + castedAt String + // Account address of the voter + voterAddress String + castedVotes VoteCast[] + votingRound VotingRound @relation(fields: [votingRoundId], references: [id]) + votingRoundId String +} + +model VoteCast { + id String @id + voteId String + vote Vote @relation(fields: [voteId], references: [id]) + questionOptionId String + questionOption VotingRoundQuestionOption @relation(fields: [questionOptionId], references: [id]) + optionIndex Int + voteWeight String +} + +model Watermark { + id String @id + watermark Int + updated String +} diff --git a/examples/xgov-voting/types/voting-app-client.ts b/examples/xgov-voting/types/voting-app-client.ts new file mode 100644 index 0000000..229fd35 --- /dev/null +++ b/examples/xgov-voting/types/voting-app-client.ts @@ -0,0 +1,1311 @@ +/* eslint-disable */ +/** + * This file was automatically generated by @algorandfoundation/algokit-client-generator. + * DO NOT MODIFY IT BY HAND. + * requires: @algorandfoundation/algokit-utils: ^2 + */ +import * as algokit from '@algorandfoundation/algokit-utils' +import type { + ABIAppCallArg, + AppCallTransactionResult, + AppCallTransactionResultOfType, + AppCompilationResult, + AppReference, + AppState, + AppStorageSchema, + CoreAppCallArgs, + RawAppCallArgs, + TealTemplateParams, +} from '@algorandfoundation/algokit-utils/types/app' +import type { + AppClientCallCoreParams, + AppClientCompilationParams, + AppClientDeployCoreParams, + AppDetails, + ApplicationClient, +} from '@algorandfoundation/algokit-utils/types/app-client' +import type { AppSpec } from '@algorandfoundation/algokit-utils/types/app-spec' +import type { + SendTransactionResult, + TransactionToSign, + SendTransactionFrom, + SendTransactionParams, +} from '@algorandfoundation/algokit-utils/types/transaction' +import type { ABIResult, TransactionWithSigner } from 'algosdk' +import { Algodv2, OnApplicationComplete, Transaction, AtomicTransactionComposer, modelsv2 } from 'algosdk' +export const APP_SPEC: AppSpec = { + hints: { + 'opup_bootstrap(pay)uint64': { + call_config: { + no_op: 'CALL', + }, + }, + 'create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void': { + call_config: { + no_op: 'CREATE', + }, + }, + 'bootstrap(pay)void': { + call_config: { + no_op: 'CALL', + }, + }, + 'close(application)void': { + default_arguments: { + opup_app: { + source: 'global-state', + data: 'ouaid', + }, + }, + call_config: { + no_op: 'CALL', + }, + }, + 'get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)': { + read_only: true, + default_arguments: { + opup_app: { + source: 'global-state', + data: 'ouaid', + }, + }, + structs: { + output: { + name: 'VotingPreconditions', + elements: [ + ['is_voting_open', 'uint64'], + ['is_allowed_to_vote', 'uint64'], + ['has_already_voted', 'uint64'], + ['current_time', 'uint64'], + ], + }, + }, + call_config: { + no_op: 'CALL', + }, + }, + 'vote(pay,byte[],uint64,uint8[],uint64[],application)void': { + default_arguments: { + opup_app: { + source: 'global-state', + data: 'ouaid', + }, + }, + call_config: { + no_op: 'CALL', + }, + }, + }, + source: { + approval: + 'I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAxMCAzCmJ5dGVjYmxvY2sgMHg3NjZmNzQ2NTVmNzQ3OTcwNjUgMHggMHg2Zjc1NjE2OTY0IDB4NzY2Zjc0NjU1ZjY5NjQgMHg2ZjcwNzQ2OTZmNmU1ZjYzNmY3NTZlNzQ3MyAweDY5NzM1ZjYyNmY2Zjc0NzM3NDcyNjE3MDcwNjU2NCAweDc2NmY3NDY1NzI1ZjYzNmY3NTZlNzQgMHg2MzZjNmY3MzY1NWY3NDY5NmQ2NSAweDc0NmY3NDYxNmM1ZjZmNzA3NDY5NmY2ZTczIDB4NTYgMHg3MzZlNjE3MDczNjg2Zjc0NWY3MDc1NjI2YzY5NjM1ZjZiNjU3OSAweDZkNjU3NDYxNjQ2MTc0NjE1ZjY5NzA2NjczNWY2MzY5NjQgMHg3Mzc0NjE3Mjc0NWY3NDY5NmQ2NSAweDY1NmU2NDVmNzQ2OTZkNjUgMHg3MTc1NmY3Mjc1NmQgMHg2ZTY2NzQ1ZjY5NmQ2MTY3NjU1Zjc1NzI2YyAweDRjNmJlYTcyIDB4MTUxZjdjNzUgMHg2ZTY2NzQ1ZjYxNzM3MzY1NzQ1ZjY5NjQgMHgwNjgxMDEgMHgyYwp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMTQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxMDFjZWEwMCAvLyAib3B1cF9ib290c3RyYXAocGF5KXVpbnQ2NCIKPT0KYm56IG1haW5fbDEzCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4NWQ0Y2YwNjYgLy8gImNyZWF0ZShzdHJpbmcsdWludDgsYnl0ZVtdLHN0cmluZyx1aW50NjQsdWludDY0LHVpbnQ4W10sdWludDY0LHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMTIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhNGU4ZDE2NCAvLyAiYm9vdHN0cmFwKHBheSl2b2lkIgo9PQpibnogbWFpbl9sMTEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg5NTQ2ZTEwZiAvLyAiY2xvc2UoYXBwbGljYXRpb24pdm9pZCIKPT0KYm56IG1haW5fbDEwCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MzYzMzA4MjQgLy8gImdldF9wcmVjb25kaXRpb25zKGJ5dGVbXSx1aW50NjQsYXBwbGljYXRpb24pKHVpbnQ2NCx1aW50NjQsdWludDY0LHVpbnQ2NCkiCj09CmJueiBtYWluX2w5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YzQwZmZkYWEgLy8gInZvdGUocGF5LGJ5dGVbXSx1aW50NjQsdWludDhbXSx1aW50NjRbXSxhcHBsaWNhdGlvbil2b2lkIgo9PQpibnogbWFpbl9sOAplcnIKbWFpbl9sODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpzdG9yZSAxNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAyCmJ0b2kKc3RvcmUgMTgKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwpzdG9yZSAxOQp0eG5hIEFwcGxpY2F0aW9uQXJncyA0CnN0b3JlIDIwCnR4bmEgQXBwbGljYXRpb25BcmdzIDUKaW50Y18wIC8vIDAKZ2V0Ynl0ZQpzdG9yZSAyMQp0eG4gR3JvdXBJbmRleAppbnRjXzEgLy8gMQotCnN0b3JlIDE2CmxvYWQgMTYKZ3R4bnMgVHlwZUVudW0KaW50Y18xIC8vIHBheQo9PQphc3NlcnQKbG9hZCAxNgpsb2FkIDE3CmxvYWQgMTgKbG9hZCAxOQpsb2FkIDIwCmxvYWQgMjEKY2FsbHN1YiB2b3RlXzEyCmludGNfMSAvLyAxCnJldHVybgptYWluX2w5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCnN0b3JlIDEyCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKYnRvaQpzdG9yZSAxMwp0eG5hIEFwcGxpY2F0aW9uQXJncyAzCmludGNfMCAvLyAwCmdldGJ5dGUKc3RvcmUgMTQKbG9hZCAxMgpsb2FkIDEzCmxvYWQgMTQKY2FsbHN1YiBnZXRwcmVjb25kaXRpb25zXzExCnN0b3JlIDE1CmJ5dGVjIDE3IC8vIDB4MTUxZjdjNzUKbG9hZCAxNQpjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQppbnRjXzAgLy8gMApnZXRieXRlCmNhbGxzdWIgY2xvc2VfNwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4biBHcm91cEluZGV4CmludGNfMSAvLyAxCi0Kc3RvcmUgMTEKbG9hZCAxMQpndHhucyBUeXBlRW51bQppbnRjXzEgLy8gcGF5Cj09CmFzc2VydApsb2FkIDExCmNhbGxzdWIgYm9vdHN0cmFwXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDEyOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCnN0b3JlIDIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgppbnRjXzAgLy8gMApnZXRieXRlCnN0b3JlIDMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwpzdG9yZSA0CnR4bmEgQXBwbGljYXRpb25BcmdzIDQKc3RvcmUgNQp0eG5hIEFwcGxpY2F0aW9uQXJncyA1CmJ0b2kKc3RvcmUgNgp0eG5hIEFwcGxpY2F0aW9uQXJncyA2CmJ0b2kKc3RvcmUgNwp0eG5hIEFwcGxpY2F0aW9uQXJncyA3CnN0b3JlIDgKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgOApidG9pCnN0b3JlIDkKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgOQpzdG9yZSAxMApsb2FkIDIKbG9hZCAzCmxvYWQgNApsb2FkIDUKbG9hZCA2CmxvYWQgNwpsb2FkIDgKbG9hZCA5CmxvYWQgMTAKY2FsbHN1YiBjcmVhdGVfNQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTM6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4biBHcm91cEluZGV4CmludGNfMSAvLyAxCi0Kc3RvcmUgMApsb2FkIDAKZ3R4bnMgVHlwZUVudW0KaW50Y18xIC8vIHBheQo9PQphc3NlcnQKbG9hZCAwCmNhbGxzdWIgb3B1cGJvb3RzdHJhcF8zCnN0b3JlIDEKYnl0ZWMgMTcgLy8gMHgxNTFmN2M3NQpsb2FkIDEKaXRvYgpjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxNDoKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDE2CmVycgptYWluX2wxNjoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KYXNzZXJ0CmNhbGxzdWIgZGVsZXRlXzIKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBpbnRfdG9fYXNjaWkKaW50dG9hc2NpaV8wOgpwcm90byAxIDEKcHVzaGJ5dGVzIDB4MzAzMTMyMzMzNDM1MzYzNzM4MzkgLy8gIjAxMjM0NTY3ODkiCmZyYW1lX2RpZyAtMQppbnRjXzEgLy8gMQpleHRyYWN0MwpyZXRzdWIKCi8vIGl0b2EKaXRvYV8xOgpwcm90byAxIDEKZnJhbWVfZGlnIC0xCmludGNfMCAvLyAwCj09CmJueiBpdG9hXzFfbDUKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAovCmludGNfMCAvLyAwCj4KYm56IGl0b2FfMV9sNApieXRlY18xIC8vICIiCml0b2FfMV9sMzoKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAolCmNhbGxzdWIgaW50dG9hc2NpaV8wCmNvbmNhdApiIGl0b2FfMV9sNgppdG9hXzFfbDQ6CmZyYW1lX2RpZyAtMQppbnRjXzIgLy8gMTAKLwpjYWxsc3ViIGl0b2FfMQpiIGl0b2FfMV9sMwppdG9hXzFfbDU6CnB1c2hieXRlcyAweDMwIC8vICIwIgppdG9hXzFfbDY6CnJldHN1YgoKLy8gZGVsZXRlCmRlbGV0ZV8yOgpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApwdXNoaW50IFRNUExfREVMRVRBQkxFIC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gb3B1cF9ib290c3RyYXAKb3B1cGJvb3RzdHJhcF8zOgpwcm90byAxIDEKaW50Y18wIC8vIDAKZnJhbWVfZGlnIC0xCmd0eG5zIEFtb3VudApwdXNoaW50IDEwMDAwMCAvLyAxMDAwMDAKPj0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlb3B1cF80CmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBjcmVhdGVfb3B1cApjcmVhdGVvcHVwXzQ6CnByb3RvIDAgMAppdHhuX2JlZ2luCnB1c2hpbnQgNiAvLyBhcHBsCml0eG5fZmllbGQgVHlwZUVudW0KcHVzaGJ5dGVzIDB4MDgyMDAyMDAwMTMxMWIyMjEyNDAwMDFkMzYxYTAwODAwNDRjNmJlYTcyMTI0MDAwMDEwMDMxMTkyMjEyMzExODIyMTMxMDQ0ODgwMDExMjM0MzMxMTkyMjEyNDAwMDAxMDAzMTE4MjIxMjQ0MjM0MzhhMDAwMDMxMDAzMjA5MTI0NDIzNDMgLy8gMHgwODIwMDIwMDAxMzExYjIyMTI0MDAwMWQzNjFhMDA4MDA0NGM2YmVhNzIxMjQwMDAwMTAwMzExOTIyMTIzMTE4MjIxMzEwNDQ4ODAwMTEyMzQzMzExOTIyMTI0MDAwMDEwMDMxMTgyMjEyNDQyMzQzOGEwMDAwMzEwMDMyMDkxMjQ0MjM0MwppdHhuX2ZpZWxkIEFwcHJvdmFsUHJvZ3JhbQpwdXNoYnl0ZXMgMHgwODgxMDA0MyAvLyAweDA4ODEwMDQzCml0eG5fZmllbGQgQ2xlYXJTdGF0ZVByb2dyYW0KaW50Y18wIC8vIDAKaXR4bl9maWVsZCBGZWUKaXR4bl9zdWJtaXQKaW50Y18wIC8vIDAKYnl0ZWNfMiAvLyAib3VhaWQiCmFwcF9nbG9iYWxfZ2V0X2V4CnN0b3JlIDIzCnN0b3JlIDIyCmxvYWQgMjMKIQphc3NlcnQKYnl0ZWNfMiAvLyAib3VhaWQiCml0eG4gQ3JlYXRlZEFwcGxpY2F0aW9uSUQKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGUKY3JlYXRlXzU6CnByb3RvIDkgMAppbnRjXzAgLy8gMApkdXAKYnl0ZWNfMSAvLyAiIgppbnRjXzAgLy8gMApkdXBuIDIKZnJhbWVfZGlnIC01CmZyYW1lX2RpZyAtNAo8PQovLyBFbmQgdGltZSBzaG91bGQgYmUgYWZ0ZXIgc3RhcnQgdGltZQphc3NlcnQKZnJhbWVfZGlnIC00Cmdsb2JhbCBMYXRlc3RUaW1lc3RhbXAKPj0KLy8gRW5kIHRpbWUgc2hvdWxkIGJlIGluIHRoZSBmdXR1cmUKYXNzZXJ0CmZyYW1lX2RpZyAtOAppbnRjXzMgLy8gMwo8PQovLyBWb3RlIHR5cGUgc2hvdWxkIGJlIDw9IDMKYXNzZXJ0CmludGNfMCAvLyAwCmJ5dGVjXzMgLy8gInZvdGVfaWQiCmFwcF9nbG9iYWxfZ2V0X2V4CnN0b3JlIDI1CnN0b3JlIDI0CmxvYWQgMjUKIQphc3NlcnQKYnl0ZWNfMyAvLyAidm90ZV9pZCIKZnJhbWVfZGlnIC05CmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmludGNfMCAvLyAwCmJ5dGVjXzAgLy8gInZvdGVfdHlwZSIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMjcKc3RvcmUgMjYKbG9hZCAyNwohCmFzc2VydApieXRlY18wIC8vICJ2b3RlX3R5cGUiCmZyYW1lX2RpZyAtOAphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMApieXRlYyAxMCAvLyAic25hcHNob3RfcHVibGljX2tleSIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMjkKc3RvcmUgMjgKbG9hZCAyOQohCmFzc2VydApieXRlYyAxMCAvLyAic25hcHNob3RfcHVibGljX2tleSIKZnJhbWVfZGlnIC03CmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmludGNfMCAvLyAwCmJ5dGVjIDExIC8vICJtZXRhZGF0YV9pcGZzX2NpZCIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMzEKc3RvcmUgMzAKbG9hZCAzMQohCmFzc2VydApieXRlYyAxMSAvLyAibWV0YWRhdGFfaXBmc19jaWQiCmZyYW1lX2RpZyAtNgpleHRyYWN0IDIgMAphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMApieXRlYyAxMiAvLyAic3RhcnRfdGltZSIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMzMKc3RvcmUgMzIKbG9hZCAzMwohCmFzc2VydApieXRlYyAxMiAvLyAic3RhcnRfdGltZSIKZnJhbWVfZGlnIC01CmFwcF9nbG9iYWxfcHV0CmludGNfMCAvLyAwCmJ5dGVjIDEzIC8vICJlbmRfdGltZSIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMzUKc3RvcmUgMzQKbG9hZCAzNQohCmFzc2VydApieXRlYyAxMyAvLyAiZW5kX3RpbWUiCmZyYW1lX2RpZyAtNAphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMApieXRlYyAxNCAvLyAicXVvcnVtIgphcHBfZ2xvYmFsX2dldF9leApzdG9yZSAzNwpzdG9yZSAzNgpsb2FkIDM3CiEKYXNzZXJ0CmJ5dGVjIDE0IC8vICJxdW9ydW0iCmZyYW1lX2RpZyAtMgphcHBfZ2xvYmFsX3B1dApieXRlYyA1IC8vICJpc19ib290c3RyYXBwZWQiCmludGNfMCAvLyAwCmFwcF9nbG9iYWxfcHV0CmJ5dGVjIDYgLy8gInZvdGVyX2NvdW50IgppbnRjXzAgLy8gMAphcHBfZ2xvYmFsX3B1dApieXRlYyA3IC8vICJjbG9zZV90aW1lIgppbnRjXzAgLy8gMAphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMApieXRlYyAxNSAvLyAibmZ0X2ltYWdlX3VybCIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMzkKc3RvcmUgMzgKbG9hZCAzOQohCmFzc2VydApieXRlYyAxNSAvLyAibmZ0X2ltYWdlX3VybCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmJ5dGVjIDE4IC8vICJuZnRfYXNzZXRfaWQiCmludGNfMCAvLyAwCmFwcF9nbG9iYWxfcHV0CmZyYW1lX2RpZyAtMwppbnRjXzAgLy8gMApleHRyYWN0X3VpbnQxNgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKLy8gb3B0aW9uX2NvdW50cyBzaG91bGQgYmUgbm9uLWVtcHR5CmFzc2VydApmcmFtZV9kaWcgLTMKaW50Y18wIC8vIDAKZXh0cmFjdF91aW50MTYKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCnB1c2hpbnQgMTEyIC8vIDExMgo8PQovLyBDYW4ndCBoYXZlIG1vcmUgdGhhbiAxMTIgcXVlc3Rpb25zCmFzc2VydAppbnRjXzAgLy8gMApieXRlYyA0IC8vICJvcHRpb25fY291bnRzIgphcHBfZ2xvYmFsX2dldF9leApzdG9yZSA0MQpzdG9yZSA0MApsb2FkIDQxCiEKYXNzZXJ0CmJ5dGVjIDQgLy8gIm9wdGlvbl9jb3VudHMiCmZyYW1lX2RpZyAtMwphcHBfZ2xvYmFsX3B1dApieXRlYyA0IC8vICJvcHRpb25fY291bnRzIgphcHBfZ2xvYmFsX2dldApmcmFtZV9idXJ5IDIKaW50Y18wIC8vIDAKc3RvcmUgNDMKZnJhbWVfZGlnIDIKaW50Y18wIC8vIDAKZXh0cmFjdF91aW50MTYKZnJhbWVfYnVyeSAzCmZyYW1lX2RpZyAzCnN0b3JlIDQ0CmludGNfMCAvLyAwCnN0b3JlIDQ1CmNyZWF0ZV81X2wxOgpsb2FkIDQ1CmxvYWQgNDQKPApieiBjcmVhdGVfNV9sNwpnbG9iYWwgT3Bjb2RlQnVkZ2V0CnB1c2hpbnQgMTAwIC8vIDEwMAo8CmJueiBjcmVhdGVfNV9sNApjcmVhdGVfNV9sMzoKZnJhbWVfZGlnIDIKaW50Y18xIC8vIDEKbG9hZCA0NQoqCnB1c2hpbnQgMiAvLyAyCisKZ2V0Ynl0ZQpmcmFtZV9idXJ5IDQKbG9hZCA0MwpmcmFtZV9kaWcgNAorCnN0b3JlIDQzCmxvYWQgNDUKaW50Y18xIC8vIDEKKwpzdG9yZSA0NQpiIGNyZWF0ZV81X2wxCmNyZWF0ZV81X2w0OgpwdXNoaW50IDYwMCAvLyA2MDAKaW50Y18yIC8vIDEwCisKc3RvcmUgNDYKY3JlYXRlXzVfbDU6CmxvYWQgNDYKZ2xvYmFsIE9wY29kZUJ1ZGdldAo+CmJ6IGNyZWF0ZV81X2wzCml0eG5fYmVnaW4KcHVzaGludCA2IC8vIGFwcGwKaXR4bl9maWVsZCBUeXBlRW51bQppbnRjXzAgLy8gMAppdHhuX2ZpZWxkIEZlZQpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KaXR4bl9maWVsZCBPbkNvbXBsZXRpb24KYnl0ZWMgMTkgLy8gMHgwNjgxMDEKaXR4bl9maWVsZCBBcHByb3ZhbFByb2dyYW0KYnl0ZWMgMTkgLy8gMHgwNjgxMDEKaXR4bl9maWVsZCBDbGVhclN0YXRlUHJvZ3JhbQppdHhuX3N1Ym1pdApiIGNyZWF0ZV81X2w1CmNyZWF0ZV81X2w3Ogpsb2FkIDQzCnN0b3JlIDQyCmxvYWQgNDIKcHVzaGludCAxMjggLy8gMTI4Cjw9Ci8vIENhbid0IGhhdmUgbW9yZSB0aGFuIDEyOCB2b3RlIG9wdGlvbnMKYXNzZXJ0CmludGNfMCAvLyAwCmJ5dGVjIDggLy8gInRvdGFsX29wdGlvbnMiCmFwcF9nbG9iYWxfZ2V0X2V4CnN0b3JlIDQ4CnN0b3JlIDQ3CmxvYWQgNDgKIQphc3NlcnQKYnl0ZWMgOCAvLyAidG90YWxfb3B0aW9ucyIKbG9hZCA0MgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGJvb3RzdHJhcApib290c3RyYXBfNjoKcHJvdG8gMSAwCmludGNfMCAvLyAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKYnl0ZWMgNSAvLyAiaXNfYm9vdHN0cmFwcGVkIgphcHBfZ2xvYmFsX2dldAohCi8vIEFscmVhZHkgYm9vdHN0cmFwcGVkCmFzc2VydApieXRlYyA1IC8vICJpc19ib290c3RyYXBwZWQiCmludGNfMSAvLyAxCmFwcF9nbG9iYWxfcHV0CnB1c2hpbnQgMzAzOTAwIC8vIDMwMzkwMApieXRlYyA4IC8vICJ0b3RhbF9vcHRpb25zIgphcHBfZ2xvYmFsX2dldApwdXNoaW50IDMyMDAgLy8gMzIwMAoqCisKc3RvcmUgNDkKZnJhbWVfZGlnIC0xCmd0eG5zIFJlY2VpdmVyCmdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCj09Ci8vIFBheW1lbnQgbXVzdCBiZSB0byBhcHAgYWRkcmVzcwphc3NlcnQKbG9hZCA0OQppdG9iCmxvZwpmcmFtZV9kaWcgLTEKZ3R4bnMgQW1vdW50CmxvYWQgNDkKPT0KLy8gUGF5bWVudCBtdXN0IGJlIGZvciB0aGUgZXhhY3QgbWluIGJhbGFuY2UgcmVxdWlyZW1lbnQKYXNzZXJ0CmJ5dGVjIDkgLy8gIlYiCmJ5dGVjIDggLy8gInRvdGFsX29wdGlvbnMiCmFwcF9nbG9iYWxfZ2V0CnB1c2hpbnQgOCAvLyA4CioKYm94X2NyZWF0ZQpwb3AKY2FsbHN1YiBjcmVhdGVvcHVwXzQKcmV0c3ViCgovLyBjbG9zZQpjbG9zZV83Ogpwcm90byAxIDAKYnl0ZWNfMSAvLyAiIgppbnRjXzAgLy8gMApkdXBuIDIKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApmcmFtZV9kaWcgLTEKdHhuYXMgQXBwbGljYXRpb25zCmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldAo9PQovLyBPcFVwIGFwcCBJRCBub3QgcGFzc2VkIGluCmFzc2VydApwdXNoaW50IDIwMDAwIC8vIDIwMDAwCmludGNfMiAvLyAxMAorCnN0b3JlIDUwCmNsb3NlXzdfbDE6CmxvYWQgNTAKZ2xvYmFsIE9wY29kZUJ1ZGdldAo+CmJueiBjbG9zZV83X2wxNwpieXRlYyA3IC8vICJjbG9zZV90aW1lIgphcHBfZ2xvYmFsX2dldAppbnRjXzAgLy8gMAo9PQovLyBBbHJlYWR5IGNsb3NlZAphc3NlcnQKYnl0ZWMgNyAvLyAiY2xvc2VfdGltZSIKZ2xvYmFsIExhdGVzdFRpbWVzdGFtcAphcHBfZ2xvYmFsX3B1dApwdXNoYnl0ZXMgMHg3YjIyNzM3NDYxNmU2NDYxNzI2NDIyM2EyMjYxNzI2MzM2MzkyMjJjMjI2NDY1NzM2MzcyNjk3MDc0Njk2ZjZlMjIzYTIyNTQ2ODY5NzMyMDY5NzMyMDYxMjA3NjZmNzQ2OTZlNjcyMDcyNjU3Mzc1NmM3NDIwNGU0NjU0MjA2NjZmNzIyMDc2NmY3NDY5NmU2NzIwNzI2Zjc1NmU2NDIwNzc2OTc0NjgyMDQ5NDQyMCAvLyAie1wic3RhbmRhcmRcIjpcImFyYzY5XCIsXCJkZXNjcmlwdGlvblwiOlwiVGhpcyBpcyBhIHZvdGluZyByZXN1bHQgTkZUIGZvciB2b3Rpbmcgcm91bmQgd2l0aCBJRCAiCmJ5dGVjXzMgLy8gInZvdGVfaWQiCmFwcF9nbG9iYWxfZ2V0CmNvbmNhdApwdXNoYnl0ZXMgMHgyZTIyMmMyMjcwNzI2ZjcwNjU3Mjc0Njk2NTczMjIzYTdiMjI2ZDY1NzQ2MTY0NjE3NDYxMjIzYTIyNjk3MDY2NzMzYTJmMmYgLy8gIi5cIixcInByb3BlcnRpZXNcIjp7XCJtZXRhZGF0YVwiOlwiaXBmczovLyIKY29uY2F0CmJ5dGVjIDExIC8vICJtZXRhZGF0YV9pcGZzX2NpZCIKYXBwX2dsb2JhbF9nZXQKY29uY2F0CnB1c2hieXRlcyAweDIyMmMyMjY5NjQyMjNhMjIgLy8gIlwiLFwiaWRcIjpcIiIKY29uY2F0CmJ5dGVjXzMgLy8gInZvdGVfaWQiCmFwcF9nbG9iYWxfZ2V0CmNvbmNhdApwdXNoYnl0ZXMgMHgyMjJjMjI3MTc1NmY3Mjc1NmQyMjNhIC8vICJcIixcInF1b3J1bVwiOiIKY29uY2F0CmJ5dGVjIDE0IC8vICJxdW9ydW0iCmFwcF9nbG9iYWxfZ2V0CmNhbGxzdWIgaXRvYV8xCmNvbmNhdApwdXNoYnl0ZXMgMHgyYzIyNzY2Zjc0NjU3MjQzNmY3NTZlNzQyMjNhIC8vICIsXCJ2b3RlckNvdW50XCI6Igpjb25jYXQKYnl0ZWMgNiAvLyAidm90ZXJfY291bnQiCmFwcF9nbG9iYWxfZ2V0CmNhbGxzdWIgaXRvYV8xCmNvbmNhdApwdXNoYnl0ZXMgMHgyYzIyNzQ2MTZjNmM2OTY1NzMyMjNhNWIgLy8gIixcInRhbGxpZXNcIjpbIgpjb25jYXQKc3RvcmUgNTEKYnl0ZWMgNCAvLyAib3B0aW9uX2NvdW50cyIKYXBwX2dsb2JhbF9nZXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmludGNfMCAvLyAwCmV4dHJhY3RfdWludDE2CmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpzdG9yZSA1MgppbnRjXzAgLy8gMApzdG9yZSA1MwppbnRjXzAgLy8gMApzdG9yZSA1NAppbnRjXzAgLy8gMApzdG9yZSA1NQpjbG9zZV83X2wzOgpsb2FkIDU1CmxvYWQgNTIKPApieiBjbG9zZV83X2wxOApmcmFtZV9kaWcgMAppbnRjXzEgLy8gMQpsb2FkIDU1CioKcHVzaGludCAyIC8vIDIKKwpnZXRieXRlCmZyYW1lX2J1cnkgMgpmcmFtZV9kaWcgMgpzdG9yZSA1NgppbnRjXzAgLy8gMApzdG9yZSA1NwpjbG9zZV83X2w1Ogpsb2FkIDU3CmxvYWQgNTYKPApibnogY2xvc2VfN19sNwpsb2FkIDU1CmludGNfMSAvLyAxCisKc3RvcmUgNTUKYiBjbG9zZV83X2wzCmNsb3NlXzdfbDc6CnB1c2hpbnQgOCAvLyA4CmxvYWQgNTQKKgpzdG9yZSA1OApieXRlYyA5IC8vICJWIgpsb2FkIDU4CnB1c2hpbnQgOCAvLyA4CmJveF9leHRyYWN0CmJ0b2kKc3RvcmUgNTMKbG9hZCA1MQpsb2FkIDU3CmludGNfMCAvLyAwCj09CmJueiBjbG9zZV83X2wxNgpieXRlY18xIC8vICIiCmNsb3NlXzdfbDk6CmNvbmNhdApsb2FkIDUzCmNhbGxzdWIgaXRvYV8xCmNvbmNhdApsb2FkIDU3CmxvYWQgNTYKaW50Y18xIC8vIDEKLQo9PQpibnogY2xvc2VfN19sMTIKYnl0ZWMgMjAgLy8gIiwiCmNsb3NlXzdfbDExOgpjb25jYXQKc3RvcmUgNTEKbG9hZCA1NAppbnRjXzEgLy8gMQorCnN0b3JlIDU0CmxvYWQgNTcKaW50Y18xIC8vIDEKKwpzdG9yZSA1NwpiIGNsb3NlXzdfbDUKY2xvc2VfN19sMTI6CnB1c2hieXRlcyAweDVkIC8vICJdIgpsb2FkIDU1CmxvYWQgNTIKaW50Y18xIC8vIDEKLQo9PQpibnogY2xvc2VfN19sMTUKYnl0ZWMgMjAgLy8gIiwiCmNsb3NlXzdfbDE0Ogpjb25jYXQKYiBjbG9zZV83X2wxMQpjbG9zZV83X2wxNToKYnl0ZWNfMSAvLyAiIgpiIGNsb3NlXzdfbDE0CmNsb3NlXzdfbDE2OgpwdXNoYnl0ZXMgMHg1YiAvLyAiWyIKYiBjbG9zZV83X2w5CmNsb3NlXzdfbDE3OgppdHhuX2JlZ2luCnB1c2hpbnQgNiAvLyBhcHBsCml0eG5fZmllbGQgVHlwZUVudW0KYnl0ZWNfMiAvLyAib3VhaWQiCmFwcF9nbG9iYWxfZ2V0Cml0eG5fZmllbGQgQXBwbGljYXRpb25JRApieXRlYyAxNiAvLyAib3B1cCgpdm9pZCIKaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKaW50Y18wIC8vIDAKaXR4bl9maWVsZCBGZWUKaXR4bl9zdWJtaXQKYiBjbG9zZV83X2wxCmNsb3NlXzdfbDE4OgppdHhuX2JlZ2luCmludGNfMyAvLyBhY2ZnCml0eG5fZmllbGQgVHlwZUVudW0KaW50Y18xIC8vIDEKaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCmludGNfMCAvLyAwCml0eG5fZmllbGQgQ29uZmlnQXNzZXREZWNpbWFscwppbnRjXzAgLy8gMAppdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0RGVmYXVsdEZyb3plbgpwdXNoYnl0ZXMgMHg1YjU2NGY1NDQ1MjA1MjQ1NTM1NTRjNTQ1ZDIwIC8vICJbVk9URSBSRVNVTFRdICIKYnl0ZWNfMyAvLyAidm90ZV9pZCIKYXBwX2dsb2JhbF9nZXQKY29uY2F0Cml0eG5fZmllbGQgQ29uZmlnQXNzZXROYW1lCnB1c2hieXRlcyAweDU2NGY1NDQ1NTI1MzRjNTQgLy8gIlZPVEVSU0xUIgppdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VW5pdE5hbWUKYnl0ZWMgMTUgLy8gIm5mdF9pbWFnZV91cmwiCmFwcF9nbG9iYWxfZ2V0Cml0eG5fZmllbGQgQ29uZmlnQXNzZXRVUkwKbG9hZCA1MQpwdXNoYnl0ZXMgMHg1ZDdkN2QgLy8gIl19fSIKY29uY2F0Cml0eG5fZmllbGQgTm90ZQppdHhuX3N1Ym1pdApieXRlYyAxOCAvLyAibmZ0X2Fzc2V0X2lkIgppdHhuIENyZWF0ZWRBc3NldElECmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gYWxsb3dlZF90b192b3RlCmFsbG93ZWR0b3ZvdGVfODoKcHJvdG8gMyAxCmJ5dGVjXzAgLy8gInZvdGVfdHlwZSIKYXBwX2dsb2JhbF9nZXQKaW50Y18wIC8vIDAKPT0KYm56IGFsbG93ZWR0b3ZvdGVfOF9sOApmcmFtZV9kaWcgLTEKdHhuYXMgQXBwbGljYXRpb25zCmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldAo9PQovLyBPcFVwIGFwcCBJRCBub3QgcGFzc2VkIGluCmFzc2VydApwdXNoaW50IDIwMDAgLy8gMjAwMAppbnRjXzIgLy8gMTAKKwpzdG9yZSA1OQphbGxvd2VkdG92b3RlXzhfbDI6CmxvYWQgNTkKZ2xvYmFsIE9wY29kZUJ1ZGdldAo+CmJueiBhbGxvd2VkdG92b3RlXzhfbDcKYnl0ZWNfMCAvLyAidm90ZV90eXBlIgphcHBfZ2xvYmFsX2dldAppbnRjXzEgLy8gMQo9PQpibnogYWxsb3dlZHRvdm90ZV84X2w2CnR4biBTZW5kZXIKZnJhbWVfZGlnIC0yCml0b2IKY29uY2F0CmFsbG93ZWR0b3ZvdGVfOF9sNToKZnJhbWVfZGlnIC0zCmJ5dGVjIDEwIC8vICJzbmFwc2hvdF9wdWJsaWNfa2V5IgphcHBfZ2xvYmFsX2dldAplZDI1NTE5dmVyaWZ5X2JhcmUKYiBhbGxvd2VkdG92b3RlXzhfbDkKYWxsb3dlZHRvdm90ZV84X2w2Ogp0eG4gU2VuZGVyCmIgYWxsb3dlZHRvdm90ZV84X2w1CmFsbG93ZWR0b3ZvdGVfOF9sNzoKaXR4bl9iZWdpbgpwdXNoaW50IDYgLy8gYXBwbAppdHhuX2ZpZWxkIFR5cGVFbnVtCmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldAppdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKYnl0ZWMgMTYgLy8gIm9wdXAoKXZvaWQiCml0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCmludGNfMCAvLyAwCml0eG5fZmllbGQgRmVlCml0eG5fc3VibWl0CmIgYWxsb3dlZHRvdm90ZV84X2wyCmFsbG93ZWR0b3ZvdGVfOF9sODoKaW50Y18xIC8vIDEKYWxsb3dlZHRvdm90ZV84X2w5OgpyZXRzdWIKCi8vIHZvdGluZ19vcGVuCnZvdGluZ29wZW5fOToKcHJvdG8gMCAxCmJ5dGVjIDUgLy8gImlzX2Jvb3RzdHJhcHBlZCIKYXBwX2dsb2JhbF9nZXQKaW50Y18xIC8vIDEKPT0KYnl0ZWMgNyAvLyAiY2xvc2VfdGltZSIKYXBwX2dsb2JhbF9nZXQKaW50Y18wIC8vIDAKPT0KJiYKZ2xvYmFsIExhdGVzdFRpbWVzdGFtcApieXRlYyAxMiAvLyAic3RhcnRfdGltZSIKYXBwX2dsb2JhbF9nZXQKPj0KJiYKZ2xvYmFsIExhdGVzdFRpbWVzdGFtcApieXRlYyAxMyAvLyAiZW5kX3RpbWUiCmFwcF9nbG9iYWxfZ2V0CjwKJiYKcmV0c3ViCgovLyBhbHJlYWR5X3ZvdGVkCmFscmVhZHl2b3RlZF8xMDoKcHJvdG8gMCAxCmJ5dGVjXzEgLy8gIiIKdHhuIFNlbmRlcgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCnB1c2hpbnQgMzIgLy8gMzIKPT0KYXNzZXJ0CmZyYW1lX2RpZyAwCmJveF9sZW4Kc3RvcmUgNjEKc3RvcmUgNjAKbG9hZCA2MQpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfcHJlY29uZGl0aW9ucwpnZXRwcmVjb25kaXRpb25zXzExOgpwcm90byAzIDEKYnl0ZWNfMSAvLyAiIgppbnRjXzAgLy8gMApkdXBuIDUKYnl0ZWNfMSAvLyAiIgpkdXAKY2FsbHN1YiB2b3RpbmdvcGVuXzkKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAtMwpleHRyYWN0IDIgMApmcmFtZV9kaWcgLTIKZnJhbWVfZGlnIC0xCmNhbGxzdWIgYWxsb3dlZHRvdm90ZV84CmZyYW1lX2J1cnkgMgpjYWxsc3ViIGFscmVhZHl2b3RlZF8xMApmcmFtZV9idXJ5IDMKZ2xvYmFsIExhdGVzdFRpbWVzdGFtcApmcmFtZV9idXJ5IDQKZnJhbWVfZGlnIDEKaXRvYgpmcmFtZV9kaWcgMgppdG9iCmNvbmNhdApmcmFtZV9kaWcgMwppdG9iCmNvbmNhdApmcmFtZV9kaWcgNAppdG9iCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB2b3RlCnZvdGVfMTI6CnByb3RvIDYgMApieXRlY18xIC8vICIiCmludGNfMCAvLyAwCmR1cG4gMTEKYnl0ZWNfMSAvLyAiIgpmcmFtZV9kaWcgLTEKdHhuYXMgQXBwbGljYXRpb25zCmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldAo9PQovLyBPcFVwIGFwcCBJRCBub3QgcGFzc2VkIGluCmFzc2VydApmcmFtZV9kaWcgLTUKZXh0cmFjdCAyIDAKZnJhbWVfZGlnIC00CmZyYW1lX2RpZyAtMQpjYWxsc3ViIGFsbG93ZWR0b3ZvdGVfOAovLyBOb3QgYWxsb3dlZCB0byB2b3RlCmFzc2VydApjYWxsc3ViIHZvdGluZ29wZW5fOQovLyBWb3Rpbmcgbm90IG9wZW4KYXNzZXJ0CmNhbGxzdWIgYWxyZWFkeXZvdGVkXzEwCiEKLy8gQWxyZWFkeSB2b3RlZAphc3NlcnQKYnl0ZWMgNCAvLyAib3B0aW9uX2NvdW50cyIKYXBwX2dsb2JhbF9nZXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmludGNfMCAvLyAwCmV4dHJhY3RfdWludDE2CmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpzdG9yZSA2MgpmcmFtZV9kaWcgLTMKaW50Y18wIC8vIDAKZXh0cmFjdF91aW50MTYKZnJhbWVfYnVyeSAyCmZyYW1lX2RpZyAyCmxvYWQgNjIKPT0KLy8gTnVtYmVyIG9mIGFuc3dlcnMgaW5jb3JyZWN0CmFzc2VydApieXRlY18wIC8vICJ2b3RlX3R5cGUiCmFwcF9nbG9iYWxfZ2V0CmludGNfMyAvLyAzCj09CmJueiB2b3RlXzEyX2wyMApmcmFtZV9kaWcgLTIKaW50Y18wIC8vIDAKZXh0cmFjdF91aW50MTYKZnJhbWVfYnVyeSA0CmZyYW1lX2RpZyA0CmludGNfMCAvLyAwCj09Ci8vIE51bWJlciBvZiBhbnN3ZXIgd2VpZ2h0cyBzaG91bGQgYmUgMCBzaW5jZSB0aGlzIHZvdGUgZG9lc24ndCB1c2UgcGFydGl0aW9uZWQgd2VpZ2h0aW5nCmFzc2VydAp2b3RlXzEyX2wyOgpwdXNoaW50IDI1MDAgLy8gMjUwMApwdXNoaW50IDM0IC8vIDM0CmludGNfMSAvLyAxCmZyYW1lX2RpZyAtMwppbnRjXzAgLy8gMApleHRyYWN0X3VpbnQxNgpmcmFtZV9idXJ5IDYKZnJhbWVfZGlnIDYKKgorCnB1c2hpbnQgNDAwIC8vIDQwMAoqCisKc3RvcmUgNjMKZnJhbWVfZGlnIC02Cmd0eG5zIFJlY2VpdmVyCmdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCj09Ci8vIFBheW1lbnQgbXVzdCBiZSB0byBhcHAgYWRkcmVzcwphc3NlcnQKbG9hZCA2MwppdG9iCmxvZwpmcmFtZV9kaWcgLTYKZ3R4bnMgQW1vdW50CmxvYWQgNjMKPT0KLy8gUGF5bWVudCBtdXN0IGJlIHRoZSBleGFjdCBtaW4gYmFsYW5jZSByZXF1aXJlbWVudAphc3NlcnQKaW50Y18wIC8vIDAKc3RvcmUgNjQKaW50Y18wIC8vIDAKc3RvcmUgNjUKaW50Y18wIC8vIDAKc3RvcmUgNjYKdm90ZV8xMl9sMzoKbG9hZCA2Ngpsb2FkIDYyCjwKYm56IHZvdGVfMTJfbDYKYnl0ZWNfMCAvLyAidm90ZV90eXBlIgphcHBfZ2xvYmFsX2dldAppbnRjXzMgLy8gMwo9PQpieiB2b3RlXzEyX2wyMQpsb2FkIDY1CmZyYW1lX2RpZyAtNAo9PQovLyBEaWRuJ3QgcGFydGl0aW9uIGV4YWN0IHZvdGluZyB3ZWlnaHQgYWNyb3NzIHF1ZXN0aW9ucwphc3NlcnQKYiB2b3RlXzEyX2wyMQp2b3RlXzEyX2w2OgpnbG9iYWwgT3Bjb2RlQnVkZ2V0CnB1c2hpbnQgMTAwIC8vIDEwMAo8CmJueiB2b3RlXzEyX2wxNwp2b3RlXzEyX2w3OgpmcmFtZV9kaWcgLTMKaW50Y18xIC8vIDEKbG9hZCA2NgoqCnB1c2hpbnQgMiAvLyAyCisKZ2V0Ynl0ZQpmcmFtZV9idXJ5IDcKaW50Y18wIC8vIDAKZnJhbWVfYnVyeSA5CmJ5dGVjXzAgLy8gInZvdGVfdHlwZSIKYXBwX2dsb2JhbF9nZXQKaW50Y18zIC8vIDMKPT0KYm56IHZvdGVfMTJfbDE2CnZvdGVfMTJfbDg6CmZyYW1lX2RpZyAwCmludGNfMSAvLyAxCmxvYWQgNjYKKgpwdXNoaW50IDIgLy8gMgorCmdldGJ5dGUKZnJhbWVfYnVyeSAxMQpmcmFtZV9kaWcgNwpmcmFtZV9kaWcgMTEKPAovLyBBbnN3ZXIgb3B0aW9uIGluZGV4IGludmFsaWQKYXNzZXJ0CnB1c2hpbnQgOCAvLyA4CmxvYWQgNjQKZnJhbWVfZGlnIDcKKwoqCnN0b3JlIDY4CmJ5dGVjIDkgLy8gIlYiCmxvYWQgNjgKcHVzaGludCA4IC8vIDgKYm94X2V4dHJhY3QKYnRvaQpzdG9yZSA2OQpieXRlYyA5IC8vICJWIgpsb2FkIDY4CmxvYWQgNjkKYnl0ZWNfMCAvLyAidm90ZV90eXBlIgphcHBfZ2xvYmFsX2dldAppbnRjXzAgLy8gMAo9PQpieXRlY18wIC8vICJ2b3RlX3R5cGUiCmFwcF9nbG9iYWxfZ2V0CmludGNfMSAvLyAxCj09Cnx8CmJueiB2b3RlXzEyX2wxNQpieXRlY18wIC8vICJ2b3RlX3R5cGUiCmFwcF9nbG9iYWxfZ2V0CnB1c2hpbnQgMiAvLyAyCj09CmJueiB2b3RlXzEyX2wxNApmcmFtZV9kaWcgOQp2b3RlXzEyX2wxMToKKwppdG9iCmJveF9yZXBsYWNlCmxvYWQgNjQKZnJhbWVfZGlnIDExCisKc3RvcmUgNjQKYnl0ZWNfMCAvLyAidm90ZV90eXBlIgphcHBfZ2xvYmFsX2dldAppbnRjXzMgLy8gMwo9PQpibnogdm90ZV8xMl9sMTMKdm90ZV8xMl9sMTI6CmxvYWQgNjYKaW50Y18xIC8vIDEKKwpzdG9yZSA2NgpiIHZvdGVfMTJfbDMKdm90ZV8xMl9sMTM6CmxvYWQgNjUKZnJhbWVfZGlnIDkKKwpzdG9yZSA2NQpiIHZvdGVfMTJfbDEyCnZvdGVfMTJfbDE0OgpmcmFtZV9kaWcgLTQKYiB2b3RlXzEyX2wxMQp2b3RlXzEyX2wxNToKaW50Y18xIC8vIDEKYiB2b3RlXzEyX2wxMQp2b3RlXzEyX2wxNjoKZnJhbWVfZGlnIC0yCnB1c2hpbnQgOCAvLyA4CmxvYWQgNjYKKgpwdXNoaW50IDIgLy8gMgorCmV4dHJhY3RfdWludDY0CmZyYW1lX2J1cnkgOQpiIHZvdGVfMTJfbDgKdm90ZV8xMl9sMTc6CnB1c2hpbnQgNjgwIC8vIDY4MAppbnRjXzIgLy8gMTAKKwpzdG9yZSA2Nwp2b3RlXzEyX2wxODoKbG9hZCA2NwpnbG9iYWwgT3Bjb2RlQnVkZ2V0Cj4KYnogdm90ZV8xMl9sNwppdHhuX2JlZ2luCnB1c2hpbnQgNiAvLyBhcHBsCml0eG5fZmllbGQgVHlwZUVudW0KYnl0ZWNfMiAvLyAib3VhaWQiCmFwcF9nbG9iYWxfZ2V0Cml0eG5fZmllbGQgQXBwbGljYXRpb25JRApieXRlYyAxNiAvLyAib3B1cCgpdm9pZCIKaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKaW50Y18wIC8vIDAKaXR4bl9maWVsZCBGZWUKaXR4bl9zdWJtaXQKYiB2b3RlXzEyX2wxOAp2b3RlXzEyX2wyMDoKZnJhbWVfZGlnIC0yCmludGNfMCAvLyAwCmV4dHJhY3RfdWludDE2CmZyYW1lX2J1cnkgMwpmcmFtZV9kaWcgMwpsb2FkIDYyCj09Ci8vIE51bWJlciBvZiBhbnN3ZXIgd2VpZ2h0cyBpbmNvcnJlY3QsIHNob3VsZCBtYXRjaCBudW1iZXIgb2YgcXVlc3Rpb25zIHNpbmNlIHRoaXMgdm90ZSB1c2VzIHBhcnRpdGlvbmVkIHdlaWdodGluZwphc3NlcnQKYiB2b3RlXzEyX2wyCnZvdGVfMTJfbDIxOgp0eG4gU2VuZGVyCmZyYW1lX2J1cnkgMTMKZnJhbWVfZGlnIDEzCmxlbgpwdXNoaW50IDMyIC8vIDMyCj09CmFzc2VydApmcmFtZV9kaWcgMTMKYm94X2RlbApwb3AKZnJhbWVfZGlnIDEzCmZyYW1lX2RpZyAtMwpib3hfcHV0CmJ5dGVjIDYgLy8gInZvdGVyX2NvdW50IgpieXRlYyA2IC8vICJ2b3Rlcl9jb3VudCIKYXBwX2dsb2JhbF9nZXQKaW50Y18xIC8vIDEKKwphcHBfZ2xvYmFsX3B1dApyZXRzdWI=', + clear: 'I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu', + }, + state: { + global: { + num_byte_slices: 5, + num_uints: 10, + }, + local: { + num_byte_slices: 0, + num_uints: 0, + }, + }, + schema: { + global: { + declared: { + close_time: { + type: 'uint64', + key: 'close_time', + descr: 'The unix timestamp of the time the vote was closed', + }, + end_time: { + type: 'uint64', + key: 'end_time', + descr: 'The unix timestamp of the ending time of voting', + }, + is_bootstrapped: { + type: 'uint64', + key: 'is_bootstrapped', + descr: 'Whether or not the contract has been bootstrapped with answers', + }, + metadata_ipfs_cid: { + type: 'bytes', + key: 'metadata_ipfs_cid', + descr: 'The IPFS content ID of the voting metadata file', + }, + nft_asset_id: { + type: 'uint64', + key: 'nft_asset_id', + descr: 'The asset ID of a result NFT if one has been created', + }, + nft_image_url: { + type: 'bytes', + key: 'nft_image_url', + descr: 'The IPFS URL of the default image to use as the media of the result NFT', + }, + option_counts: { + type: 'bytes', + key: 'option_counts', + descr: 'The number of options for each question', + }, + opup_app_id: { + type: 'uint64', + key: 'ouaid', + descr: '', + }, + quorum: { + type: 'uint64', + key: 'quorum', + descr: 'The minimum number of voters to reach quorum', + }, + snapshot_public_key: { + type: 'bytes', + key: 'snapshot_public_key', + descr: 'The public key of the Ed25519 compatible private key that was used to encrypt entries in the vote gating snapshot', + }, + start_time: { + type: 'uint64', + key: 'start_time', + descr: 'The unix timestamp of the starting time of voting', + }, + total_options: { + type: 'uint64', + key: 'total_options', + descr: 'The total number of options', + }, + vote_id: { + type: 'bytes', + key: 'vote_id', + descr: 'The identifier of this voting round', + }, + vote_type: { + type: 'uint64', + key: 'vote_type', + descr: + 'The type of this voting round; 0 = no snapshot / weighting, 1 = snapshot & no weighting, 2 = snapshot & weighting per question, 3 = snapshot & weighting partitioned across the questions', + }, + voter_count: { + type: 'uint64', + key: 'voter_count', + descr: 'The minimum number of voters who have voted', + }, + }, + reserved: {}, + }, + local: { + declared: {}, + reserved: {}, + }, + }, + contract: { + name: 'VotingRoundApp', + methods: [ + { + name: 'opup_bootstrap', + args: [ + { + type: 'pay', + name: 'ptxn', + }, + ], + returns: { + type: 'uint64', + }, + desc: 'initialize opup with bootstrap to create a target app', + }, + { + name: 'create', + args: [ + { + type: 'string', + name: 'vote_id', + }, + { + type: 'uint8', + name: 'vote_type', + }, + { + type: 'byte[]', + name: 'snapshot_public_key', + }, + { + type: 'string', + name: 'metadata_ipfs_cid', + }, + { + type: 'uint64', + name: 'start_time', + }, + { + type: 'uint64', + name: 'end_time', + }, + { + type: 'uint8[]', + name: 'option_counts', + }, + { + type: 'uint64', + name: 'quorum', + }, + { + type: 'string', + name: 'nft_image_url', + }, + ], + returns: { + type: 'void', + }, + }, + { + name: 'bootstrap', + args: [ + { + type: 'pay', + name: 'fund_min_bal_req', + }, + ], + returns: { + type: 'void', + }, + }, + { + name: 'close', + args: [ + { + type: 'application', + name: 'opup_app', + }, + ], + returns: { + type: 'void', + }, + }, + { + name: 'get_preconditions', + args: [ + { + type: 'byte[]', + name: 'signature', + }, + { + type: 'uint64', + name: 'weighting', + }, + { + type: 'application', + name: 'opup_app', + }, + ], + returns: { + type: '(uint64,uint64,uint64,uint64)', + }, + }, + { + name: 'vote', + args: [ + { + type: 'pay', + name: 'fund_min_bal_req', + }, + { + type: 'byte[]', + name: 'signature', + }, + { + type: 'uint64', + name: 'weighting', + }, + { + type: 'uint8[]', + name: 'answer_ids', + }, + { + type: 'uint64[]', + name: 'answer_weights', + }, + { + type: 'application', + name: 'opup_app', + }, + ], + returns: { + type: 'void', + }, + }, + ], + networks: {}, + }, + bare_call_config: { + delete_application: 'CALL', + }, +} + +/** + * Defines an onCompletionAction of 'no_op' + */ +export type OnCompleteNoOp = { onCompleteAction?: 'no_op' | OnApplicationComplete.NoOpOC } +/** + * Defines an onCompletionAction of 'opt_in' + */ +export type OnCompleteOptIn = { onCompleteAction: 'opt_in' | OnApplicationComplete.OptInOC } +/** + * Defines an onCompletionAction of 'close_out' + */ +export type OnCompleteCloseOut = { onCompleteAction: 'close_out' | OnApplicationComplete.CloseOutOC } +/** + * Defines an onCompletionAction of 'delete_application' + */ +export type OnCompleteDelApp = { onCompleteAction: 'delete_application' | OnApplicationComplete.DeleteApplicationOC } +/** + * Defines an onCompletionAction of 'update_application' + */ +export type OnCompleteUpdApp = { onCompleteAction: 'update_application' | OnApplicationComplete.UpdateApplicationOC } +/** + * A state record containing a single unsigned integer + */ +export type IntegerState = { + /** + * Gets the state value as a BigInt. + */ + asBigInt(): bigint + /** + * Gets the state value as a number. + */ + asNumber(): number +} +/** + * A state record containing binary data + */ +export type BinaryState = { + /** + * Gets the state value as a Uint8Array + */ + asByteArray(): Uint8Array + /** + * Gets the state value as a string + */ + asString(): string +} + +export type AppCreateCallTransactionResult = AppCallTransactionResult & Partial & AppReference +export type AppUpdateCallTransactionResult = AppCallTransactionResult & Partial + +export type AppClientComposeCallCoreParams = Omit & { + sendParams?: Omit< + SendTransactionParams, + 'skipSending' | 'atc' | 'skipWaiting' | 'maxRoundsToWaitForConfirmation' | 'populateAppCallResources' + > +} +export type AppClientComposeExecuteParams = Pick< + SendTransactionParams, + 'skipWaiting' | 'maxRoundsToWaitForConfirmation' | 'populateAppCallResources' | 'suppressLog' +> + +export type IncludeSchema = { + /** + * Any overrides for the storage schema to request for the created app; by default the schema indicated by the app spec is used. + */ + schema?: Partial +} + +/** + * Defines the types of available calls and state of the VotingRoundApp smart contract. + */ +export type VotingRoundApp = { + /** + * Maps method signatures / names to their argument and return types. + */ + methods: Record< + 'opup_bootstrap(pay)uint64' | 'opup_bootstrap', + { + argsObj: { + ptxn: TransactionToSign | Transaction | Promise + } + argsTuple: [ptxn: TransactionToSign | Transaction | Promise] + returns: bigint + } + > & + Record< + 'create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void' | 'create', + { + argsObj: { + voteId: string + voteType: number + snapshotPublicKey: Uint8Array + metadataIpfsCid: string + startTime: bigint | number + endTime: bigint | number + optionCounts: number[] + quorum: bigint | number + nftImageUrl: string + } + argsTuple: [ + voteId: string, + voteType: number, + snapshotPublicKey: Uint8Array, + metadataIpfsCid: string, + startTime: bigint | number, + endTime: bigint | number, + optionCounts: number[], + quorum: bigint | number, + nftImageUrl: string, + ] + returns: void + } + > & + Record< + 'bootstrap(pay)void' | 'bootstrap', + { + argsObj: { + fundMinBalReq: TransactionToSign | Transaction | Promise + } + argsTuple: [fundMinBalReq: TransactionToSign | Transaction | Promise] + returns: void + } + > & + Record< + 'close(application)void' | 'close', + { + argsObj: { + opupApp?: number | bigint + } + argsTuple: [opupApp: number | bigint | undefined] + returns: void + } + > & + Record< + 'get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)' | 'get_preconditions', + { + argsObj: { + signature: Uint8Array + weighting: bigint | number + opupApp?: number | bigint + } + argsTuple: [signature: Uint8Array, weighting: bigint | number, opupApp: number | bigint | undefined] + returns: VotingPreconditions + } + > & + Record< + 'vote(pay,byte[],uint64,uint8[],uint64[],application)void' | 'vote', + { + argsObj: { + fundMinBalReq: TransactionToSign | Transaction | Promise + signature: Uint8Array + weighting: bigint | number + answerIds: number[] + answerWeights: bigint | number[] + opupApp?: number | bigint + } + argsTuple: [ + fundMinBalReq: TransactionToSign | Transaction | Promise, + signature: Uint8Array, + weighting: bigint | number, + answerIds: number[], + answerWeights: bigint | number[], + opupApp: number | bigint | undefined, + ] + returns: void + } + > + /** + * Defines the shape of the global and local state of the application. + */ + state: { + global: { + /** + * The unix timestamp of the time the vote was closed + */ + closeTime?: IntegerState + /** + * The unix timestamp of the ending time of voting + */ + endTime?: IntegerState + /** + * Whether or not the contract has been bootstrapped with answers + */ + isBootstrapped?: IntegerState + /** + * The IPFS content ID of the voting metadata file + */ + metadataIpfsCid?: BinaryState + /** + * The asset ID of a result NFT if one has been created + */ + nftAssetId?: IntegerState + /** + * The IPFS URL of the default image to use as the media of the result NFT + */ + nftImageUrl?: BinaryState + /** + * The number of options for each question + */ + optionCounts?: BinaryState + ouaid?: IntegerState + /** + * The minimum number of voters to reach quorum + */ + quorum?: IntegerState + /** + * The public key of the Ed25519 compatible private key that was used to encrypt entries in the vote gating snapshot + */ + snapshotPublicKey?: BinaryState + /** + * The unix timestamp of the starting time of voting + */ + startTime?: IntegerState + /** + * The total number of options + */ + totalOptions?: IntegerState + /** + * The identifier of this voting round + */ + voteId?: BinaryState + /** + * The type of this voting round; 0 = no snapshot / weighting, 1 = snapshot & no weighting, 2 = snapshot & weighting per question, 3 = snapshot & weighting partitioned across the questions + */ + voteType?: IntegerState + /** + * The minimum number of voters who have voted + */ + voterCount?: IntegerState + } + } +} +/** + * Defines the possible abi call signatures + */ +export type VotingRoundAppSig = keyof VotingRoundApp['methods'] +/** + * Defines an object containing all relevant parameters for a single call to the contract. Where TSignature is undefined, a bare call is made + */ +export type TypedCallParams = { + method: TSignature + methodArgs: TSignature extends undefined ? undefined : Array +} & AppClientCallCoreParams & + CoreAppCallArgs +/** + * Defines the arguments required for a bare call + */ +export type BareCallArgs = Omit +/** + * Represents a VotingPreconditions result as a struct + */ +export type VotingPreconditions = { + isVotingOpen: bigint + isAllowedToVote: bigint + hasAlreadyVoted: bigint + currentTime: bigint +} +/** + * Converts the tuple representation of a VotingPreconditions to the struct representation + */ +export function VotingPreconditions([isVotingOpen, isAllowedToVote, hasAlreadyVoted, currentTime]: [bigint, bigint, bigint, bigint]) { + return { + isVotingOpen, + isAllowedToVote, + hasAlreadyVoted, + currentTime, + } +} +/** + * Maps a method signature from the VotingRoundApp smart contract to the method's arguments in either tuple of struct form + */ +export type MethodArgs = VotingRoundApp['methods'][TSignature]['argsObj' | 'argsTuple'] +/** + * Maps a method signature from the VotingRoundApp smart contract to the method's return type + */ +export type MethodReturn = VotingRoundApp['methods'][TSignature]['returns'] + +/** + * A factory for available 'create' calls + */ +export type VotingRoundAppCreateCalls = (typeof VotingRoundAppCallFactory)['create'] +/** + * Defines supported create methods for this smart contract + */ +export type VotingRoundAppCreateCallParams = TypedCallParams<'create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void'> & + OnCompleteNoOp +/** + * A factory for available 'delete' calls + */ +export type VotingRoundAppDeleteCalls = (typeof VotingRoundAppCallFactory)['delete'] +/** + * Defines supported delete methods for this smart contract + */ +export type VotingRoundAppDeleteCallParams = TypedCallParams +/** + * Defines arguments required for the deploy method. + */ +export type VotingRoundAppDeployArgs = { + deployTimeParams?: TealTemplateParams + /** + * A delegate which takes a create call factory and returns the create call params for this smart contract + */ + createCall?: (callFactory: VotingRoundAppCreateCalls) => VotingRoundAppCreateCallParams + /** + * A delegate which takes a delete call factory and returns the delete call params for this smart contract + */ + deleteCall?: (callFactory: VotingRoundAppDeleteCalls) => VotingRoundAppDeleteCallParams +} + +/** + * Exposes methods for constructing all available smart contract calls + */ +export abstract class VotingRoundAppCallFactory { + /** + * Gets available create call factories + */ + static get create() { + return { + /** + * Constructs a create call for the VotingRoundApp smart contract using the create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void ABI method + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + create( + args: MethodArgs<'create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void'>, + params: AppClientCallCoreParams & CoreAppCallArgs & AppClientCompilationParams & OnCompleteNoOp = {}, + ) { + return { + method: 'create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void' as const, + methodArgs: Array.isArray(args) + ? args + : [ + args.voteId, + args.voteType, + args.snapshotPublicKey, + args.metadataIpfsCid, + args.startTime, + args.endTime, + args.optionCounts, + args.quorum, + args.nftImageUrl, + ], + ...params, + } + }, + } + } + + /** + * Gets available delete call factories + */ + static get delete() { + return { + /** + * Constructs a delete call for the VotingRoundApp smart contract using a bare call + * + * @param params Any parameters for the call + * @returns A TypedCallParams object for the call + */ + bare(params: BareCallArgs & AppClientCallCoreParams & CoreAppCallArgs = {}) { + return { + method: undefined, + methodArgs: undefined, + ...params, + } + }, + } + } + + /** + * Constructs a no op call for the opup_bootstrap(pay)uint64 ABI method + * + * initialize opup with bootstrap to create a target app + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + static opupBootstrap(args: MethodArgs<'opup_bootstrap(pay)uint64'>, params: AppClientCallCoreParams & CoreAppCallArgs) { + return { + method: 'opup_bootstrap(pay)uint64' as const, + methodArgs: Array.isArray(args) ? args : [args.ptxn], + ...params, + } + } + /** + * Constructs a no op call for the bootstrap(pay)void ABI method + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + static bootstrap(args: MethodArgs<'bootstrap(pay)void'>, params: AppClientCallCoreParams & CoreAppCallArgs) { + return { + method: 'bootstrap(pay)void' as const, + methodArgs: Array.isArray(args) ? args : [args.fundMinBalReq], + ...params, + } + } + /** + * Constructs a no op call for the close(application)void ABI method + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + static close(args: MethodArgs<'close(application)void'>, params: AppClientCallCoreParams & CoreAppCallArgs) { + return { + method: 'close(application)void' as const, + methodArgs: Array.isArray(args) ? args : [args.opupApp], + ...params, + } + } + /** + * Constructs a no op call for the get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64) ABI method + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + static getPreconditions( + args: MethodArgs<'get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)'>, + params: AppClientCallCoreParams & CoreAppCallArgs, + ) { + return { + method: 'get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)' as const, + methodArgs: Array.isArray(args) ? args : [args.signature, args.weighting, args.opupApp], + ...params, + } + } + /** + * Constructs a no op call for the vote(pay,byte[],uint64,uint8[],uint64[],application)void ABI method + * + * @param args Any args for the contract call + * @param params Any additional parameters for the call + * @returns A TypedCallParams object for the call + */ + static vote( + args: MethodArgs<'vote(pay,byte[],uint64,uint8[],uint64[],application)void'>, + params: AppClientCallCoreParams & CoreAppCallArgs, + ) { + return { + method: 'vote(pay,byte[],uint64,uint8[],uint64[],application)void' as const, + methodArgs: Array.isArray(args) + ? args + : [args.fundMinBalReq, args.signature, args.weighting, args.answerIds, args.answerWeights, args.opupApp], + ...params, + } + } +} + +/** + * A client to make calls to the VotingRoundApp smart contract + */ +export class VotingRoundAppClient { + /** + * The underlying `ApplicationClient` for when you want to have more flexibility + */ + public readonly appClient: ApplicationClient + + private readonly sender: SendTransactionFrom | undefined + + /** + * Creates a new instance of `VotingRoundAppClient` + * + * @param appDetails appDetails The details to identify the app to deploy + * @param algod An algod client instance + */ + constructor( + appDetails: AppDetails, + private algod: Algodv2, + ) { + this.sender = appDetails.sender + this.appClient = algokit.getAppClient( + { + ...appDetails, + app: APP_SPEC, + }, + algod, + ) + } + + /** + * Checks for decode errors on the AppCallTransactionResult and maps the return value to the specified generic type + * + * @param result The AppCallTransactionResult to be mapped + * @param returnValueFormatter An optional delegate to format the return value if required + * @returns The smart contract response with an updated return value + */ + protected mapReturnValue( + result: AppCallTransactionResult, + returnValueFormatter?: (value: any) => TReturn, + ): AppCallTransactionResultOfType & TResult { + if (result.return?.decodeError) { + throw result.return.decodeError + } + const returnValue = + result.return?.returnValue !== undefined && returnValueFormatter !== undefined + ? returnValueFormatter(result.return.returnValue) + : (result.return?.returnValue as TReturn | undefined) + return { ...result, return: returnValue } as AppCallTransactionResultOfType & TResult + } + + /** + * Calls the ABI method with the matching signature using an onCompletion code of NO_OP + * + * @param typedCallParams An object containing the method signature, args, and any other relevant parameters + * @param returnValueFormatter An optional delegate which when provided will be used to map non-undefined return values to the target type + * @returns The result of the smart contract call + */ + public async call( + typedCallParams: TypedCallParams, + returnValueFormatter?: (value: any) => MethodReturn, + ) { + return this.mapReturnValue>(await this.appClient.call(typedCallParams), returnValueFormatter) + } + + /** + * Idempotently deploys the VotingRoundApp smart contract. + * + * @param params The arguments for the contract calls and any additional parameters for the call + * @returns The deployment result + */ + public deploy( + params: VotingRoundAppDeployArgs & AppClientDeployCoreParams & IncludeSchema = {}, + ): ReturnType { + const createArgs = params.createCall?.(VotingRoundAppCallFactory.create) + const deleteArgs = params.deleteCall?.(VotingRoundAppCallFactory.delete) + return this.appClient.deploy({ + ...params, + deleteArgs, + createArgs, + createOnCompleteAction: createArgs?.onCompleteAction, + }) + } + + /** + * Gets available create methods + */ + public get create() { + const $this = this + return { + /** + * Creates a new instance of the VotingRoundApp smart contract using the create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void ABI method. + * + * @param args The arguments for the smart contract call + * @param params Any additional parameters for the call + * @returns The create result + */ + async create( + args: MethodArgs<'create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void'>, + params: AppClientCallCoreParams & AppClientCompilationParams & IncludeSchema & CoreAppCallArgs & OnCompleteNoOp = {}, + ) { + return $this.mapReturnValue< + MethodReturn<'create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void'>, + AppCreateCallTransactionResult + >(await $this.appClient.create(VotingRoundAppCallFactory.create.create(args, params))) + }, + } + } + + /** + * Gets available delete methods + */ + public get delete() { + const $this = this + return { + /** + * Deletes an existing instance of the VotingRoundApp smart contract using a bare call. + * + * @param args The arguments for the bare call + * @returns The delete result + */ + async bare(args: BareCallArgs & AppClientCallCoreParams & CoreAppCallArgs = {}) { + return $this.mapReturnValue(await $this.appClient.delete(args)) + }, + } + } + + /** + * Makes a clear_state call to an existing instance of the VotingRoundApp smart contract. + * + * @param args The arguments for the bare call + * @returns The clear_state result + */ + public clearState(args: BareCallArgs & AppClientCallCoreParams & CoreAppCallArgs = {}) { + return this.appClient.clearState(args) + } + + /** + * Calls the opup_bootstrap(pay)uint64 ABI method. + * + * initialize opup with bootstrap to create a target app + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The result of the call + */ + public opupBootstrap(args: MethodArgs<'opup_bootstrap(pay)uint64'>, params: AppClientCallCoreParams & CoreAppCallArgs = {}) { + return this.call(VotingRoundAppCallFactory.opupBootstrap(args, params)) + } + + /** + * Calls the bootstrap(pay)void ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The result of the call + */ + public bootstrap(args: MethodArgs<'bootstrap(pay)void'>, params: AppClientCallCoreParams & CoreAppCallArgs = {}) { + return this.call(VotingRoundAppCallFactory.bootstrap(args, params)) + } + + /** + * Calls the close(application)void ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The result of the call + */ + public close(args: MethodArgs<'close(application)void'>, params: AppClientCallCoreParams & CoreAppCallArgs = {}) { + return this.call(VotingRoundAppCallFactory.close(args, params)) + } + + /** + * Calls the get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64) ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The result of the call + */ + public getPreconditions( + args: MethodArgs<'get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)'>, + params: AppClientCallCoreParams & CoreAppCallArgs = {}, + ) { + return this.call(VotingRoundAppCallFactory.getPreconditions(args, params), VotingPreconditions) + } + + /** + * Calls the vote(pay,byte[],uint64,uint8[],uint64[],application)void ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The result of the call + */ + public vote( + args: MethodArgs<'vote(pay,byte[],uint64,uint8[],uint64[],application)void'>, + params: AppClientCallCoreParams & CoreAppCallArgs = {}, + ) { + return this.call(VotingRoundAppCallFactory.vote(args, params)) + } + + /** + * Extracts a binary state value out of an AppState dictionary + * + * @param state The state dictionary containing the state value + * @param key The key of the state value + * @returns A BinaryState instance containing the state value, or undefined if the key was not found + */ + private static getBinaryState(state: AppState, key: string): BinaryState | undefined { + const value = state[key] + if (!value) return undefined + if (!('valueRaw' in value)) throw new Error(`Failed to parse state value for ${key}; received an int when expected a byte array`) + return { + asString(): string { + return value.value + }, + asByteArray(): Uint8Array { + return value.valueRaw + }, + } + } + + /** + * Extracts a integer state value out of an AppState dictionary + * + * @param state The state dictionary containing the state value + * @param key The key of the state value + * @returns An IntegerState instance containing the state value, or undefined if the key was not found + */ + private static getIntegerState(state: AppState, key: string): IntegerState | undefined { + const value = state[key] + if (!value) return undefined + if ('valueRaw' in value) throw new Error(`Failed to parse state value for ${key}; received a byte array when expected a number`) + return { + asBigInt() { + return typeof value.value === 'bigint' ? value.value : BigInt(value.value) + }, + asNumber(): number { + return typeof value.value === 'bigint' ? Number(value.value) : value.value + }, + } + } + + /** + * Returns the smart contract's global state wrapped in a strongly typed accessor with options to format the stored value + */ + public async getGlobalState(): Promise { + const state = await this.appClient.getGlobalState() + return { + get closeTime() { + return VotingRoundAppClient.getIntegerState(state, 'close_time') + }, + get endTime() { + return VotingRoundAppClient.getIntegerState(state, 'end_time') + }, + get isBootstrapped() { + return VotingRoundAppClient.getIntegerState(state, 'is_bootstrapped') + }, + get metadataIpfsCid() { + return VotingRoundAppClient.getBinaryState(state, 'metadata_ipfs_cid') + }, + get nftAssetId() { + return VotingRoundAppClient.getIntegerState(state, 'nft_asset_id') + }, + get nftImageUrl() { + return VotingRoundAppClient.getBinaryState(state, 'nft_image_url') + }, + get optionCounts() { + return VotingRoundAppClient.getBinaryState(state, 'option_counts') + }, + get ouaid() { + return VotingRoundAppClient.getIntegerState(state, 'ouaid') + }, + get quorum() { + return VotingRoundAppClient.getIntegerState(state, 'quorum') + }, + get snapshotPublicKey() { + return VotingRoundAppClient.getBinaryState(state, 'snapshot_public_key') + }, + get startTime() { + return VotingRoundAppClient.getIntegerState(state, 'start_time') + }, + get totalOptions() { + return VotingRoundAppClient.getIntegerState(state, 'total_options') + }, + get voteId() { + return VotingRoundAppClient.getBinaryState(state, 'vote_id') + }, + get voteType() { + return VotingRoundAppClient.getIntegerState(state, 'vote_type') + }, + get voterCount() { + return VotingRoundAppClient.getIntegerState(state, 'voter_count') + }, + } + } + + public compose(): VotingRoundAppComposer { + const client = this + const atc = new AtomicTransactionComposer() + let promiseChain: Promise = Promise.resolve() + const resultMappers: Array any)> = [] + return { + opupBootstrap(args: MethodArgs<'opup_bootstrap(pay)uint64'>, params?: AppClientComposeCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => + client.opupBootstrap(args, { ...params, sendParams: { ...params?.sendParams, skipSending: true, atc } }), + ) + resultMappers.push(undefined) + return this + }, + bootstrap(args: MethodArgs<'bootstrap(pay)void'>, params?: AppClientComposeCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => + client.bootstrap(args, { ...params, sendParams: { ...params?.sendParams, skipSending: true, atc } }), + ) + resultMappers.push(undefined) + return this + }, + close(args: MethodArgs<'close(application)void'>, params?: AppClientComposeCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => + client.close(args, { ...params, sendParams: { ...params?.sendParams, skipSending: true, atc } }), + ) + resultMappers.push(undefined) + return this + }, + getPreconditions( + args: MethodArgs<'get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)'>, + params?: AppClientComposeCallCoreParams & CoreAppCallArgs, + ) { + promiseChain = promiseChain.then(() => + client.getPreconditions(args, { ...params, sendParams: { ...params?.sendParams, skipSending: true, atc } }), + ) + resultMappers.push(VotingPreconditions) + return this + }, + vote( + args: MethodArgs<'vote(pay,byte[],uint64,uint8[],uint64[],application)void'>, + params?: AppClientComposeCallCoreParams & CoreAppCallArgs, + ) { + promiseChain = promiseChain.then(() => + client.vote(args, { ...params, sendParams: { ...params?.sendParams, skipSending: true, atc } }), + ) + resultMappers.push(undefined) + return this + }, + get delete() { + const $this = this + return { + bare(args?: BareCallArgs & AppClientComposeCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => + client.delete.bare({ ...args, sendParams: { ...args?.sendParams, skipSending: true, atc } }), + ) + resultMappers.push(undefined) + return $this + }, + } + }, + clearState(args?: BareCallArgs & AppClientComposeCallCoreParams & CoreAppCallArgs) { + promiseChain = promiseChain.then(() => client.clearState({ ...args, sendParams: { ...args?.sendParams, skipSending: true, atc } })) + resultMappers.push(undefined) + return this + }, + addTransaction( + txn: TransactionWithSigner | TransactionToSign | Transaction | Promise, + defaultSender?: SendTransactionFrom, + ) { + promiseChain = promiseChain.then(async () => + atc.addTransaction(await algokit.getTransactionWithSigner(txn, defaultSender ?? client.sender)), + ) + return this + }, + async atc() { + await promiseChain + return atc + }, + async simulate(options?: SimulateOptions) { + await promiseChain + const result = await atc.simulate(client.algod, new modelsv2.SimulateRequest({ txnGroups: [], ...options })) + return { + ...result, + returns: result.methodResults?.map((val, i) => + resultMappers[i] !== undefined ? resultMappers[i]!(val.returnValue) : val.returnValue, + ), + } + }, + async execute(sendParams?: AppClientComposeExecuteParams) { + await promiseChain + const result = await algokit.sendAtomicTransactionComposer({ atc, sendParams }, client.algod) + return { + ...result, + returns: result.returns?.map((val, i) => (resultMappers[i] !== undefined ? resultMappers[i]!(val.returnValue) : val.returnValue)), + } + }, + } as unknown as VotingRoundAppComposer + } +} +export type VotingRoundAppComposer = { + /** + * Calls the opup_bootstrap(pay)uint64 ABI method. + * + * initialize opup with bootstrap to create a target app + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + opupBootstrap( + args: MethodArgs<'opup_bootstrap(pay)uint64'>, + params?: AppClientComposeCallCoreParams & CoreAppCallArgs, + ): VotingRoundAppComposer<[...TReturns, MethodReturn<'opup_bootstrap(pay)uint64'>]> + + /** + * Calls the bootstrap(pay)void ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + bootstrap( + args: MethodArgs<'bootstrap(pay)void'>, + params?: AppClientComposeCallCoreParams & CoreAppCallArgs, + ): VotingRoundAppComposer<[...TReturns, MethodReturn<'bootstrap(pay)void'>]> + + /** + * Calls the close(application)void ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + close( + args: MethodArgs<'close(application)void'>, + params?: AppClientComposeCallCoreParams & CoreAppCallArgs, + ): VotingRoundAppComposer<[...TReturns, MethodReturn<'close(application)void'>]> + + /** + * Calls the get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64) ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + getPreconditions( + args: MethodArgs<'get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)'>, + params?: AppClientComposeCallCoreParams & CoreAppCallArgs, + ): VotingRoundAppComposer<[...TReturns, MethodReturn<'get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)'>]> + + /** + * Calls the vote(pay,byte[],uint64,uint8[],uint64[],application)void ABI method. + * + * @param args The arguments for the contract call + * @param params Any additional parameters for the call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + vote( + args: MethodArgs<'vote(pay,byte[],uint64,uint8[],uint64[],application)void'>, + params?: AppClientComposeCallCoreParams & CoreAppCallArgs, + ): VotingRoundAppComposer<[...TReturns, MethodReturn<'vote(pay,byte[],uint64,uint8[],uint64[],application)void'>]> + + /** + * Gets available delete methods + */ + readonly delete: { + /** + * Deletes an existing instance of the VotingRoundApp smart contract using a bare call. + * + * @param args The arguments for the bare call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + bare(args?: BareCallArgs & AppClientComposeCallCoreParams & CoreAppCallArgs): VotingRoundAppComposer<[...TReturns, undefined]> + } + + /** + * Makes a clear_state call to an existing instance of the VotingRoundApp smart contract. + * + * @param args The arguments for the bare call + * @returns The typed transaction composer so you can fluently chain multiple calls or call execute to execute all queued up transactions + */ + clearState(args?: BareCallArgs & AppClientComposeCallCoreParams & CoreAppCallArgs): VotingRoundAppComposer<[...TReturns, undefined]> + + /** + * Adds a transaction to the composer + * + * @param txn One of: A TransactionWithSigner object (returned as is), a TransactionToSign object (signer is obtained from the signer property), a Transaction object (signer is extracted from the defaultSender parameter), an async SendTransactionResult returned by one of algokit utils helpers (signer is obtained from the defaultSender parameter) + * @param defaultSender The default sender to be used to obtain a signer where the object provided to the transaction parameter does not include a signer. + */ + addTransaction( + txn: TransactionWithSigner | TransactionToSign | Transaction | Promise, + defaultSender?: SendTransactionFrom, + ): VotingRoundAppComposer + /** + * Returns the underlying AtomicTransactionComposer instance + */ + atc(): Promise + /** + * Simulates the transaction group and returns the result + */ + simulate(options?: SimulateOptions): Promise> + /** + * Executes the transaction group and returns the results + */ + execute(sendParams?: AppClientComposeExecuteParams): Promise> +} +export type SimulateOptions = Omit[0], 'txnGroups'> +export type VotingRoundAppComposerSimulateResult = { + returns: TReturns + methodResults: ABIResult[] + simulateResponse: modelsv2.SimulateResponse +} +export type VotingRoundAppComposerResults = { + returns: TReturns + groupId: string + txIds: string[] + transactions: Transaction[] +} diff --git a/examples/xgov-voting/types/voting-round.ts b/examples/xgov-voting/types/voting-round.ts new file mode 100644 index 0000000..a74ea3b --- /dev/null +++ b/examples/xgov-voting/types/voting-round.ts @@ -0,0 +1,89 @@ +// From: https://github.com/algorandfoundation/nft_voting_tool/blob/develop/src/dapp/src/shared/IPFSGateway.ts + +export enum VoteType { + NO_SNAPSHOT = 0, + NO_WEIGHTING = 1, + WEIGHTING = 2, + PARTITIONED_WEIGHTING = 3, +} + +/** A discrete opportunity for vote casters to participate in a vote for a given context, this may consist of one or more questions */ +export interface VotingRoundMetadata { + id: string + /** + * Metadata Semantic Version + */ + version?: string + type: VoteType + title: string + description?: string + /** Optional URL link to more information */ + informationUrl?: string + /** Start of voting round as an ISO8601 string */ + start: string + /** End of voting round as an ISO8601 string */ + end: string + /** Optional quorum of participants for a valid result */ + quorum?: number + /** The optional IPFS content ID of the vote gating snapshot used for this voting round */ + voteGatingSnapshotCid?: string + /** The questions being voted on as part of the voting round */ + questions: Question[] + created: CreatedMetadata + /** The total amount allocated for the community grants program aka xGov + * this is optional for backwards compatibility + */ + communityGrantAllocation?: number +} + +export interface Question { + /** UUID of the question */ + id: string + /** The question prompt text */ + prompt: string + description?: string + metadata?: { + link?: string + category?: string + focus_area?: string + threshold?: number + ask?: number + } + options: Option[] +} + +export interface Option { + /** UUID of the option */ + id: string + /** The text description of the option */ + label: string +} + +export interface VoteGatingSnapshot { + title: string + /** + * Snapshot Semantic Version + */ + version?: string + /** Base 64 encoded public key corresponding to the ephemeral private key that was created to secure this snapshot */ + publicKey: string + created: CreatedMetadata + /** The snapshot of vote gates */ + snapshot: Gate[] +} + +export interface Gate { + /** Address of the account that is gated to vote */ + address: string + /** The vote weighting of the account that is gated to vote */ + weight?: number + /** Base 64 encoded signature of `{address}{weight(uint64)|string}` with the private key of this using ED25519 */ + signature: string +} + +export interface CreatedMetadata { + /** When the record was created, in ISO8601 format */ + at: string + /** Account address of the creator */ + by: string +} diff --git a/examples/xgov-voting/types/voting.arc32.json b/examples/xgov-voting/types/voting.arc32.json new file mode 100644 index 0000000..2724751 --- /dev/null +++ b/examples/xgov-voting/types/voting.arc32.json @@ -0,0 +1,306 @@ +{ + "hints": { + "opup_bootstrap(pay)uint64": { + "call_config": { + "no_op": "CALL" + } + }, + "create(string,uint8,byte[],string,uint64,uint64,uint8[],uint64,string)void": { + "call_config": { + "no_op": "CREATE" + } + }, + "bootstrap(pay)void": { + "call_config": { + "no_op": "CALL" + } + }, + "close(application)void": { + "default_arguments": { + "opup_app": { + "source": "global-state", + "data": "ouaid" + } + }, + "call_config": { + "no_op": "CALL" + } + }, + "get_preconditions(byte[],uint64,application)(uint64,uint64,uint64,uint64)": { + "read_only": true, + "default_arguments": { + "opup_app": { + "source": "global-state", + "data": "ouaid" + } + }, + "structs": { + "output": { + "name": "VotingPreconditions", + "elements": [ + ["is_voting_open", "uint64"], + ["is_allowed_to_vote", "uint64"], + ["has_already_voted", "uint64"], + ["current_time", "uint64"] + ] + } + }, + "call_config": { + "no_op": "CALL" + } + }, + "vote(pay,byte[],uint64,uint8[],uint64[],application)void": { + "default_arguments": { + "opup_app": { + "source": "global-state", + "data": "ouaid" + } + }, + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAxMCAzCmJ5dGVjYmxvY2sgMHg3NjZmNzQ2NTVmNzQ3OTcwNjUgMHggMHg2Zjc1NjE2OTY0IDB4NzY2Zjc0NjU1ZjY5NjQgMHg2ZjcwNzQ2OTZmNmU1ZjYzNmY3NTZlNzQ3MyAweDY5NzM1ZjYyNmY2Zjc0NzM3NDcyNjE3MDcwNjU2NCAweDc2NmY3NDY1NzI1ZjYzNmY3NTZlNzQgMHg2MzZjNmY3MzY1NWY3NDY5NmQ2NSAweDc0NmY3NDYxNmM1ZjZmNzA3NDY5NmY2ZTczIDB4NTYgMHg3MzZlNjE3MDczNjg2Zjc0NWY3MDc1NjI2YzY5NjM1ZjZiNjU3OSAweDZkNjU3NDYxNjQ2MTc0NjE1ZjY5NzA2NjczNWY2MzY5NjQgMHg3Mzc0NjE3Mjc0NWY3NDY5NmQ2NSAweDY1NmU2NDVmNzQ2OTZkNjUgMHg3MTc1NmY3Mjc1NmQgMHg2ZTY2NzQ1ZjY5NmQ2MTY3NjU1Zjc1NzI2YyAweDRjNmJlYTcyIDB4MTUxZjdjNzUgMHg2ZTY2NzQ1ZjYxNzM3MzY1NzQ1ZjY5NjQgMHgwNjgxMDEgMHgyYwp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMTQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxMDFjZWEwMCAvLyAib3B1cF9ib290c3RyYXAocGF5KXVpbnQ2NCIKPT0KYm56IG1haW5fbDEzCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4NWQ0Y2YwNjYgLy8gImNyZWF0ZShzdHJpbmcsdWludDgsYnl0ZVtdLHN0cmluZyx1aW50NjQsdWludDY0LHVpbnQ4W10sdWludDY0LHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMTIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhNGU4ZDE2NCAvLyAiYm9vdHN0cmFwKHBheSl2b2lkIgo9PQpibnogbWFpbl9sMTEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg5NTQ2ZTEwZiAvLyAiY2xvc2UoYXBwbGljYXRpb24pdm9pZCIKPT0KYm56IG1haW5fbDEwCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MzYzMzA4MjQgLy8gImdldF9wcmVjb25kaXRpb25zKGJ5dGVbXSx1aW50NjQsYXBwbGljYXRpb24pKHVpbnQ2NCx1aW50NjQsdWludDY0LHVpbnQ2NCkiCj09CmJueiBtYWluX2w5CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4YzQwZmZkYWEgLy8gInZvdGUocGF5LGJ5dGVbXSx1aW50NjQsdWludDhbXSx1aW50NjRbXSxhcHBsaWNhdGlvbil2b2lkIgo9PQpibnogbWFpbl9sOAplcnIKbWFpbl9sODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpzdG9yZSAxNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAyCmJ0b2kKc3RvcmUgMTgKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwpzdG9yZSAxOQp0eG5hIEFwcGxpY2F0aW9uQXJncyA0CnN0b3JlIDIwCnR4bmEgQXBwbGljYXRpb25BcmdzIDUKaW50Y18wIC8vIDAKZ2V0Ynl0ZQpzdG9yZSAyMQp0eG4gR3JvdXBJbmRleAppbnRjXzEgLy8gMQotCnN0b3JlIDE2CmxvYWQgMTYKZ3R4bnMgVHlwZUVudW0KaW50Y18xIC8vIHBheQo9PQphc3NlcnQKbG9hZCAxNgpsb2FkIDE3CmxvYWQgMTgKbG9hZCAxOQpsb2FkIDIwCmxvYWQgMjEKY2FsbHN1YiB2b3RlXzEyCmludGNfMSAvLyAxCnJldHVybgptYWluX2w5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCnN0b3JlIDEyCnR4bmEgQXBwbGljYXRpb25BcmdzIDIKYnRvaQpzdG9yZSAxMwp0eG5hIEFwcGxpY2F0aW9uQXJncyAzCmludGNfMCAvLyAwCmdldGJ5dGUKc3RvcmUgMTQKbG9hZCAxMgpsb2FkIDEzCmxvYWQgMTQKY2FsbHN1YiBnZXRwcmVjb25kaXRpb25zXzExCnN0b3JlIDE1CmJ5dGVjIDE3IC8vIDB4MTUxZjdjNzUKbG9hZCAxNQpjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxMDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQppbnRjXzAgLy8gMApnZXRieXRlCmNhbGxzdWIgY2xvc2VfNwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4biBHcm91cEluZGV4CmludGNfMSAvLyAxCi0Kc3RvcmUgMTEKbG9hZCAxMQpndHhucyBUeXBlRW51bQppbnRjXzEgLy8gcGF5Cj09CmFzc2VydApsb2FkIDExCmNhbGxzdWIgYm9vdHN0cmFwXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDEyOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCnN0b3JlIDIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgppbnRjXzAgLy8gMApnZXRieXRlCnN0b3JlIDMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwpzdG9yZSA0CnR4bmEgQXBwbGljYXRpb25BcmdzIDQKc3RvcmUgNQp0eG5hIEFwcGxpY2F0aW9uQXJncyA1CmJ0b2kKc3RvcmUgNgp0eG5hIEFwcGxpY2F0aW9uQXJncyA2CmJ0b2kKc3RvcmUgNwp0eG5hIEFwcGxpY2F0aW9uQXJncyA3CnN0b3JlIDgKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgOApidG9pCnN0b3JlIDkKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgOQpzdG9yZSAxMApsb2FkIDIKbG9hZCAzCmxvYWQgNApsb2FkIDUKbG9hZCA2CmxvYWQgNwpsb2FkIDgKbG9hZCA5CmxvYWQgMTAKY2FsbHN1YiBjcmVhdGVfNQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTM6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4biBHcm91cEluZGV4CmludGNfMSAvLyAxCi0Kc3RvcmUgMApsb2FkIDAKZ3R4bnMgVHlwZUVudW0KaW50Y18xIC8vIHBheQo9PQphc3NlcnQKbG9hZCAwCmNhbGxzdWIgb3B1cGJvb3RzdHJhcF8zCnN0b3JlIDEKYnl0ZWMgMTcgLy8gMHgxNTFmN2M3NQpsb2FkIDEKaXRvYgpjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxNDoKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDE2CmVycgptYWluX2wxNjoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KYXNzZXJ0CmNhbGxzdWIgZGVsZXRlXzIKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBpbnRfdG9fYXNjaWkKaW50dG9hc2NpaV8wOgpwcm90byAxIDEKcHVzaGJ5dGVzIDB4MzAzMTMyMzMzNDM1MzYzNzM4MzkgLy8gIjAxMjM0NTY3ODkiCmZyYW1lX2RpZyAtMQppbnRjXzEgLy8gMQpleHRyYWN0MwpyZXRzdWIKCi8vIGl0b2EKaXRvYV8xOgpwcm90byAxIDEKZnJhbWVfZGlnIC0xCmludGNfMCAvLyAwCj09CmJueiBpdG9hXzFfbDUKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAovCmludGNfMCAvLyAwCj4KYm56IGl0b2FfMV9sNApieXRlY18xIC8vICIiCml0b2FfMV9sMzoKZnJhbWVfZGlnIC0xCmludGNfMiAvLyAxMAolCmNhbGxzdWIgaW50dG9hc2NpaV8wCmNvbmNhdApiIGl0b2FfMV9sNgppdG9hXzFfbDQ6CmZyYW1lX2RpZyAtMQppbnRjXzIgLy8gMTAKLwpjYWxsc3ViIGl0b2FfMQpiIGl0b2FfMV9sMwppdG9hXzFfbDU6CnB1c2hieXRlcyAweDMwIC8vICIwIgppdG9hXzFfbDY6CnJldHN1YgoKLy8gZGVsZXRlCmRlbGV0ZV8yOgpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApwdXNoaW50IFRNUExfREVMRVRBQkxFIC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gb3B1cF9ib290c3RyYXAKb3B1cGJvb3RzdHJhcF8zOgpwcm90byAxIDEKaW50Y18wIC8vIDAKZnJhbWVfZGlnIC0xCmd0eG5zIEFtb3VudApwdXNoaW50IDEwMDAwMCAvLyAxMDAwMDAKPj0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlb3B1cF80CmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBjcmVhdGVfb3B1cApjcmVhdGVvcHVwXzQ6CnByb3RvIDAgMAppdHhuX2JlZ2luCnB1c2hpbnQgNiAvLyBhcHBsCml0eG5fZmllbGQgVHlwZUVudW0KcHVzaGJ5dGVzIDB4MDgyMDAyMDAwMTMxMWIyMjEyNDAwMDFkMzYxYTAwODAwNDRjNmJlYTcyMTI0MDAwMDEwMDMxMTkyMjEyMzExODIyMTMxMDQ0ODgwMDExMjM0MzMxMTkyMjEyNDAwMDAxMDAzMTE4MjIxMjQ0MjM0MzhhMDAwMDMxMDAzMjA5MTI0NDIzNDMgLy8gMHgwODIwMDIwMDAxMzExYjIyMTI0MDAwMWQzNjFhMDA4MDA0NGM2YmVhNzIxMjQwMDAwMTAwMzExOTIyMTIzMTE4MjIxMzEwNDQ4ODAwMTEyMzQzMzExOTIyMTI0MDAwMDEwMDMxMTgyMjEyNDQyMzQzOGEwMDAwMzEwMDMyMDkxMjQ0MjM0MwppdHhuX2ZpZWxkIEFwcHJvdmFsUHJvZ3JhbQpwdXNoYnl0ZXMgMHgwODgxMDA0MyAvLyAweDA4ODEwMDQzCml0eG5fZmllbGQgQ2xlYXJTdGF0ZVByb2dyYW0KaW50Y18wIC8vIDAKaXR4bl9maWVsZCBGZWUKaXR4bl9zdWJtaXQKaW50Y18wIC8vIDAKYnl0ZWNfMiAvLyAib3VhaWQiCmFwcF9nbG9iYWxfZ2V0X2V4CnN0b3JlIDIzCnN0b3JlIDIyCmxvYWQgMjMKIQphc3NlcnQKYnl0ZWNfMiAvLyAib3VhaWQiCml0eG4gQ3JlYXRlZEFwcGxpY2F0aW9uSUQKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGUKY3JlYXRlXzU6CnByb3RvIDkgMAppbnRjXzAgLy8gMApkdXAKYnl0ZWNfMSAvLyAiIgppbnRjXzAgLy8gMApkdXBuIDIKZnJhbWVfZGlnIC01CmZyYW1lX2RpZyAtNAo8PQovLyBFbmQgdGltZSBzaG91bGQgYmUgYWZ0ZXIgc3RhcnQgdGltZQphc3NlcnQKZnJhbWVfZGlnIC00Cmdsb2JhbCBMYXRlc3RUaW1lc3RhbXAKPj0KLy8gRW5kIHRpbWUgc2hvdWxkIGJlIGluIHRoZSBmdXR1cmUKYXNzZXJ0CmZyYW1lX2RpZyAtOAppbnRjXzMgLy8gMwo8PQovLyBWb3RlIHR5cGUgc2hvdWxkIGJlIDw9IDMKYXNzZXJ0CmludGNfMCAvLyAwCmJ5dGVjXzMgLy8gInZvdGVfaWQiCmFwcF9nbG9iYWxfZ2V0X2V4CnN0b3JlIDI1CnN0b3JlIDI0CmxvYWQgMjUKIQphc3NlcnQKYnl0ZWNfMyAvLyAidm90ZV9pZCIKZnJhbWVfZGlnIC05CmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmludGNfMCAvLyAwCmJ5dGVjXzAgLy8gInZvdGVfdHlwZSIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMjcKc3RvcmUgMjYKbG9hZCAyNwohCmFzc2VydApieXRlY18wIC8vICJ2b3RlX3R5cGUiCmZyYW1lX2RpZyAtOAphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMApieXRlYyAxMCAvLyAic25hcHNob3RfcHVibGljX2tleSIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMjkKc3RvcmUgMjgKbG9hZCAyOQohCmFzc2VydApieXRlYyAxMCAvLyAic25hcHNob3RfcHVibGljX2tleSIKZnJhbWVfZGlnIC03CmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmludGNfMCAvLyAwCmJ5dGVjIDExIC8vICJtZXRhZGF0YV9pcGZzX2NpZCIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMzEKc3RvcmUgMzAKbG9hZCAzMQohCmFzc2VydApieXRlYyAxMSAvLyAibWV0YWRhdGFfaXBmc19jaWQiCmZyYW1lX2RpZyAtNgpleHRyYWN0IDIgMAphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMApieXRlYyAxMiAvLyAic3RhcnRfdGltZSIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMzMKc3RvcmUgMzIKbG9hZCAzMwohCmFzc2VydApieXRlYyAxMiAvLyAic3RhcnRfdGltZSIKZnJhbWVfZGlnIC01CmFwcF9nbG9iYWxfcHV0CmludGNfMCAvLyAwCmJ5dGVjIDEzIC8vICJlbmRfdGltZSIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMzUKc3RvcmUgMzQKbG9hZCAzNQohCmFzc2VydApieXRlYyAxMyAvLyAiZW5kX3RpbWUiCmZyYW1lX2RpZyAtNAphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMApieXRlYyAxNCAvLyAicXVvcnVtIgphcHBfZ2xvYmFsX2dldF9leApzdG9yZSAzNwpzdG9yZSAzNgpsb2FkIDM3CiEKYXNzZXJ0CmJ5dGVjIDE0IC8vICJxdW9ydW0iCmZyYW1lX2RpZyAtMgphcHBfZ2xvYmFsX3B1dApieXRlYyA1IC8vICJpc19ib290c3RyYXBwZWQiCmludGNfMCAvLyAwCmFwcF9nbG9iYWxfcHV0CmJ5dGVjIDYgLy8gInZvdGVyX2NvdW50IgppbnRjXzAgLy8gMAphcHBfZ2xvYmFsX3B1dApieXRlYyA3IC8vICJjbG9zZV90aW1lIgppbnRjXzAgLy8gMAphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMApieXRlYyAxNSAvLyAibmZ0X2ltYWdlX3VybCIKYXBwX2dsb2JhbF9nZXRfZXgKc3RvcmUgMzkKc3RvcmUgMzgKbG9hZCAzOQohCmFzc2VydApieXRlYyAxNSAvLyAibmZ0X2ltYWdlX3VybCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmJ5dGVjIDE4IC8vICJuZnRfYXNzZXRfaWQiCmludGNfMCAvLyAwCmFwcF9nbG9iYWxfcHV0CmZyYW1lX2RpZyAtMwppbnRjXzAgLy8gMApleHRyYWN0X3VpbnQxNgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKLy8gb3B0aW9uX2NvdW50cyBzaG91bGQgYmUgbm9uLWVtcHR5CmFzc2VydApmcmFtZV9kaWcgLTMKaW50Y18wIC8vIDAKZXh0cmFjdF91aW50MTYKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAxCnB1c2hpbnQgMTEyIC8vIDExMgo8PQovLyBDYW4ndCBoYXZlIG1vcmUgdGhhbiAxMTIgcXVlc3Rpb25zCmFzc2VydAppbnRjXzAgLy8gMApieXRlYyA0IC8vICJvcHRpb25fY291bnRzIgphcHBfZ2xvYmFsX2dldF9leApzdG9yZSA0MQpzdG9yZSA0MApsb2FkIDQxCiEKYXNzZXJ0CmJ5dGVjIDQgLy8gIm9wdGlvbl9jb3VudHMiCmZyYW1lX2RpZyAtMwphcHBfZ2xvYmFsX3B1dApieXRlYyA0IC8vICJvcHRpb25fY291bnRzIgphcHBfZ2xvYmFsX2dldApmcmFtZV9idXJ5IDIKaW50Y18wIC8vIDAKc3RvcmUgNDMKZnJhbWVfZGlnIDIKaW50Y18wIC8vIDAKZXh0cmFjdF91aW50MTYKZnJhbWVfYnVyeSAzCmZyYW1lX2RpZyAzCnN0b3JlIDQ0CmludGNfMCAvLyAwCnN0b3JlIDQ1CmNyZWF0ZV81X2wxOgpsb2FkIDQ1CmxvYWQgNDQKPApieiBjcmVhdGVfNV9sNwpnbG9iYWwgT3Bjb2RlQnVkZ2V0CnB1c2hpbnQgMTAwIC8vIDEwMAo8CmJueiBjcmVhdGVfNV9sNApjcmVhdGVfNV9sMzoKZnJhbWVfZGlnIDIKaW50Y18xIC8vIDEKbG9hZCA0NQoqCnB1c2hpbnQgMiAvLyAyCisKZ2V0Ynl0ZQpmcmFtZV9idXJ5IDQKbG9hZCA0MwpmcmFtZV9kaWcgNAorCnN0b3JlIDQzCmxvYWQgNDUKaW50Y18xIC8vIDEKKwpzdG9yZSA0NQpiIGNyZWF0ZV81X2wxCmNyZWF0ZV81X2w0OgpwdXNoaW50IDYwMCAvLyA2MDAKaW50Y18yIC8vIDEwCisKc3RvcmUgNDYKY3JlYXRlXzVfbDU6CmxvYWQgNDYKZ2xvYmFsIE9wY29kZUJ1ZGdldAo+CmJ6IGNyZWF0ZV81X2wzCml0eG5fYmVnaW4KcHVzaGludCA2IC8vIGFwcGwKaXR4bl9maWVsZCBUeXBlRW51bQppbnRjXzAgLy8gMAppdHhuX2ZpZWxkIEZlZQpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KaXR4bl9maWVsZCBPbkNvbXBsZXRpb24KYnl0ZWMgMTkgLy8gMHgwNjgxMDEKaXR4bl9maWVsZCBBcHByb3ZhbFByb2dyYW0KYnl0ZWMgMTkgLy8gMHgwNjgxMDEKaXR4bl9maWVsZCBDbGVhclN0YXRlUHJvZ3JhbQppdHhuX3N1Ym1pdApiIGNyZWF0ZV81X2w1CmNyZWF0ZV81X2w3Ogpsb2FkIDQzCnN0b3JlIDQyCmxvYWQgNDIKcHVzaGludCAxMjggLy8gMTI4Cjw9Ci8vIENhbid0IGhhdmUgbW9yZSB0aGFuIDEyOCB2b3RlIG9wdGlvbnMKYXNzZXJ0CmludGNfMCAvLyAwCmJ5dGVjIDggLy8gInRvdGFsX29wdGlvbnMiCmFwcF9nbG9iYWxfZ2V0X2V4CnN0b3JlIDQ4CnN0b3JlIDQ3CmxvYWQgNDgKIQphc3NlcnQKYnl0ZWMgOCAvLyAidG90YWxfb3B0aW9ucyIKbG9hZCA0MgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGJvb3RzdHJhcApib290c3RyYXBfNjoKcHJvdG8gMSAwCmludGNfMCAvLyAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKYnl0ZWMgNSAvLyAiaXNfYm9vdHN0cmFwcGVkIgphcHBfZ2xvYmFsX2dldAohCi8vIEFscmVhZHkgYm9vdHN0cmFwcGVkCmFzc2VydApieXRlYyA1IC8vICJpc19ib290c3RyYXBwZWQiCmludGNfMSAvLyAxCmFwcF9nbG9iYWxfcHV0CnB1c2hpbnQgMzAzOTAwIC8vIDMwMzkwMApieXRlYyA4IC8vICJ0b3RhbF9vcHRpb25zIgphcHBfZ2xvYmFsX2dldApwdXNoaW50IDMyMDAgLy8gMzIwMAoqCisKc3RvcmUgNDkKZnJhbWVfZGlnIC0xCmd0eG5zIFJlY2VpdmVyCmdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCj09Ci8vIFBheW1lbnQgbXVzdCBiZSB0byBhcHAgYWRkcmVzcwphc3NlcnQKbG9hZCA0OQppdG9iCmxvZwpmcmFtZV9kaWcgLTEKZ3R4bnMgQW1vdW50CmxvYWQgNDkKPT0KLy8gUGF5bWVudCBtdXN0IGJlIGZvciB0aGUgZXhhY3QgbWluIGJhbGFuY2UgcmVxdWlyZW1lbnQKYXNzZXJ0CmJ5dGVjIDkgLy8gIlYiCmJ5dGVjIDggLy8gInRvdGFsX29wdGlvbnMiCmFwcF9nbG9iYWxfZ2V0CnB1c2hpbnQgOCAvLyA4CioKYm94X2NyZWF0ZQpwb3AKY2FsbHN1YiBjcmVhdGVvcHVwXzQKcmV0c3ViCgovLyBjbG9zZQpjbG9zZV83Ogpwcm90byAxIDAKYnl0ZWNfMSAvLyAiIgppbnRjXzAgLy8gMApkdXBuIDIKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApmcmFtZV9kaWcgLTEKdHhuYXMgQXBwbGljYXRpb25zCmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldAo9PQovLyBPcFVwIGFwcCBJRCBub3QgcGFzc2VkIGluCmFzc2VydApwdXNoaW50IDIwMDAwIC8vIDIwMDAwCmludGNfMiAvLyAxMAorCnN0b3JlIDUwCmNsb3NlXzdfbDE6CmxvYWQgNTAKZ2xvYmFsIE9wY29kZUJ1ZGdldAo+CmJueiBjbG9zZV83X2wxNwpieXRlYyA3IC8vICJjbG9zZV90aW1lIgphcHBfZ2xvYmFsX2dldAppbnRjXzAgLy8gMAo9PQovLyBBbHJlYWR5IGNsb3NlZAphc3NlcnQKYnl0ZWMgNyAvLyAiY2xvc2VfdGltZSIKZ2xvYmFsIExhdGVzdFRpbWVzdGFtcAphcHBfZ2xvYmFsX3B1dApwdXNoYnl0ZXMgMHg3YjIyNzM3NDYxNmU2NDYxNzI2NDIyM2EyMjYxNzI2MzM2MzkyMjJjMjI2NDY1NzM2MzcyNjk3MDc0Njk2ZjZlMjIzYTIyNTQ2ODY5NzMyMDY5NzMyMDYxMjA3NjZmNzQ2OTZlNjcyMDcyNjU3Mzc1NmM3NDIwNGU0NjU0MjA2NjZmNzIyMDc2NmY3NDY5NmU2NzIwNzI2Zjc1NmU2NDIwNzc2OTc0NjgyMDQ5NDQyMCAvLyAie1wic3RhbmRhcmRcIjpcImFyYzY5XCIsXCJkZXNjcmlwdGlvblwiOlwiVGhpcyBpcyBhIHZvdGluZyByZXN1bHQgTkZUIGZvciB2b3Rpbmcgcm91bmQgd2l0aCBJRCAiCmJ5dGVjXzMgLy8gInZvdGVfaWQiCmFwcF9nbG9iYWxfZ2V0CmNvbmNhdApwdXNoYnl0ZXMgMHgyZTIyMmMyMjcwNzI2ZjcwNjU3Mjc0Njk2NTczMjIzYTdiMjI2ZDY1NzQ2MTY0NjE3NDYxMjIzYTIyNjk3MDY2NzMzYTJmMmYgLy8gIi5cIixcInByb3BlcnRpZXNcIjp7XCJtZXRhZGF0YVwiOlwiaXBmczovLyIKY29uY2F0CmJ5dGVjIDExIC8vICJtZXRhZGF0YV9pcGZzX2NpZCIKYXBwX2dsb2JhbF9nZXQKY29uY2F0CnB1c2hieXRlcyAweDIyMmMyMjY5NjQyMjNhMjIgLy8gIlwiLFwiaWRcIjpcIiIKY29uY2F0CmJ5dGVjXzMgLy8gInZvdGVfaWQiCmFwcF9nbG9iYWxfZ2V0CmNvbmNhdApwdXNoYnl0ZXMgMHgyMjJjMjI3MTc1NmY3Mjc1NmQyMjNhIC8vICJcIixcInF1b3J1bVwiOiIKY29uY2F0CmJ5dGVjIDE0IC8vICJxdW9ydW0iCmFwcF9nbG9iYWxfZ2V0CmNhbGxzdWIgaXRvYV8xCmNvbmNhdApwdXNoYnl0ZXMgMHgyYzIyNzY2Zjc0NjU3MjQzNmY3NTZlNzQyMjNhIC8vICIsXCJ2b3RlckNvdW50XCI6Igpjb25jYXQKYnl0ZWMgNiAvLyAidm90ZXJfY291bnQiCmFwcF9nbG9iYWxfZ2V0CmNhbGxzdWIgaXRvYV8xCmNvbmNhdApwdXNoYnl0ZXMgMHgyYzIyNzQ2MTZjNmM2OTY1NzMyMjNhNWIgLy8gIixcInRhbGxpZXNcIjpbIgpjb25jYXQKc3RvcmUgNTEKYnl0ZWMgNCAvLyAib3B0aW9uX2NvdW50cyIKYXBwX2dsb2JhbF9nZXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmludGNfMCAvLyAwCmV4dHJhY3RfdWludDE2CmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpzdG9yZSA1MgppbnRjXzAgLy8gMApzdG9yZSA1MwppbnRjXzAgLy8gMApzdG9yZSA1NAppbnRjXzAgLy8gMApzdG9yZSA1NQpjbG9zZV83X2wzOgpsb2FkIDU1CmxvYWQgNTIKPApieiBjbG9zZV83X2wxOApmcmFtZV9kaWcgMAppbnRjXzEgLy8gMQpsb2FkIDU1CioKcHVzaGludCAyIC8vIDIKKwpnZXRieXRlCmZyYW1lX2J1cnkgMgpmcmFtZV9kaWcgMgpzdG9yZSA1NgppbnRjXzAgLy8gMApzdG9yZSA1NwpjbG9zZV83X2w1Ogpsb2FkIDU3CmxvYWQgNTYKPApibnogY2xvc2VfN19sNwpsb2FkIDU1CmludGNfMSAvLyAxCisKc3RvcmUgNTUKYiBjbG9zZV83X2wzCmNsb3NlXzdfbDc6CnB1c2hpbnQgOCAvLyA4CmxvYWQgNTQKKgpzdG9yZSA1OApieXRlYyA5IC8vICJWIgpsb2FkIDU4CnB1c2hpbnQgOCAvLyA4CmJveF9leHRyYWN0CmJ0b2kKc3RvcmUgNTMKbG9hZCA1MQpsb2FkIDU3CmludGNfMCAvLyAwCj09CmJueiBjbG9zZV83X2wxNgpieXRlY18xIC8vICIiCmNsb3NlXzdfbDk6CmNvbmNhdApsb2FkIDUzCmNhbGxzdWIgaXRvYV8xCmNvbmNhdApsb2FkIDU3CmxvYWQgNTYKaW50Y18xIC8vIDEKLQo9PQpibnogY2xvc2VfN19sMTIKYnl0ZWMgMjAgLy8gIiwiCmNsb3NlXzdfbDExOgpjb25jYXQKc3RvcmUgNTEKbG9hZCA1NAppbnRjXzEgLy8gMQorCnN0b3JlIDU0CmxvYWQgNTcKaW50Y18xIC8vIDEKKwpzdG9yZSA1NwpiIGNsb3NlXzdfbDUKY2xvc2VfN19sMTI6CnB1c2hieXRlcyAweDVkIC8vICJdIgpsb2FkIDU1CmxvYWQgNTIKaW50Y18xIC8vIDEKLQo9PQpibnogY2xvc2VfN19sMTUKYnl0ZWMgMjAgLy8gIiwiCmNsb3NlXzdfbDE0Ogpjb25jYXQKYiBjbG9zZV83X2wxMQpjbG9zZV83X2wxNToKYnl0ZWNfMSAvLyAiIgpiIGNsb3NlXzdfbDE0CmNsb3NlXzdfbDE2OgpwdXNoYnl0ZXMgMHg1YiAvLyAiWyIKYiBjbG9zZV83X2w5CmNsb3NlXzdfbDE3OgppdHhuX2JlZ2luCnB1c2hpbnQgNiAvLyBhcHBsCml0eG5fZmllbGQgVHlwZUVudW0KYnl0ZWNfMiAvLyAib3VhaWQiCmFwcF9nbG9iYWxfZ2V0Cml0eG5fZmllbGQgQXBwbGljYXRpb25JRApieXRlYyAxNiAvLyAib3B1cCgpdm9pZCIKaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKaW50Y18wIC8vIDAKaXR4bl9maWVsZCBGZWUKaXR4bl9zdWJtaXQKYiBjbG9zZV83X2wxCmNsb3NlXzdfbDE4OgppdHhuX2JlZ2luCmludGNfMyAvLyBhY2ZnCml0eG5fZmllbGQgVHlwZUVudW0KaW50Y18xIC8vIDEKaXR4bl9maWVsZCBDb25maWdBc3NldFRvdGFsCmludGNfMCAvLyAwCml0eG5fZmllbGQgQ29uZmlnQXNzZXREZWNpbWFscwppbnRjXzAgLy8gMAppdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0RGVmYXVsdEZyb3plbgpwdXNoYnl0ZXMgMHg1YjU2NGY1NDQ1MjA1MjQ1NTM1NTRjNTQ1ZDIwIC8vICJbVk9URSBSRVNVTFRdICIKYnl0ZWNfMyAvLyAidm90ZV9pZCIKYXBwX2dsb2JhbF9nZXQKY29uY2F0Cml0eG5fZmllbGQgQ29uZmlnQXNzZXROYW1lCnB1c2hieXRlcyAweDU2NGY1NDQ1NTI1MzRjNTQgLy8gIlZPVEVSU0xUIgppdHhuX2ZpZWxkIENvbmZpZ0Fzc2V0VW5pdE5hbWUKYnl0ZWMgMTUgLy8gIm5mdF9pbWFnZV91cmwiCmFwcF9nbG9iYWxfZ2V0Cml0eG5fZmllbGQgQ29uZmlnQXNzZXRVUkwKbG9hZCA1MQpwdXNoYnl0ZXMgMHg1ZDdkN2QgLy8gIl19fSIKY29uY2F0Cml0eG5fZmllbGQgTm90ZQppdHhuX3N1Ym1pdApieXRlYyAxOCAvLyAibmZ0X2Fzc2V0X2lkIgppdHhuIENyZWF0ZWRBc3NldElECmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gYWxsb3dlZF90b192b3RlCmFsbG93ZWR0b3ZvdGVfODoKcHJvdG8gMyAxCmJ5dGVjXzAgLy8gInZvdGVfdHlwZSIKYXBwX2dsb2JhbF9nZXQKaW50Y18wIC8vIDAKPT0KYm56IGFsbG93ZWR0b3ZvdGVfOF9sOApmcmFtZV9kaWcgLTEKdHhuYXMgQXBwbGljYXRpb25zCmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldAo9PQovLyBPcFVwIGFwcCBJRCBub3QgcGFzc2VkIGluCmFzc2VydApwdXNoaW50IDIwMDAgLy8gMjAwMAppbnRjXzIgLy8gMTAKKwpzdG9yZSA1OQphbGxvd2VkdG92b3RlXzhfbDI6CmxvYWQgNTkKZ2xvYmFsIE9wY29kZUJ1ZGdldAo+CmJueiBhbGxvd2VkdG92b3RlXzhfbDcKYnl0ZWNfMCAvLyAidm90ZV90eXBlIgphcHBfZ2xvYmFsX2dldAppbnRjXzEgLy8gMQo9PQpibnogYWxsb3dlZHRvdm90ZV84X2w2CnR4biBTZW5kZXIKZnJhbWVfZGlnIC0yCml0b2IKY29uY2F0CmFsbG93ZWR0b3ZvdGVfOF9sNToKZnJhbWVfZGlnIC0zCmJ5dGVjIDEwIC8vICJzbmFwc2hvdF9wdWJsaWNfa2V5IgphcHBfZ2xvYmFsX2dldAplZDI1NTE5dmVyaWZ5X2JhcmUKYiBhbGxvd2VkdG92b3RlXzhfbDkKYWxsb3dlZHRvdm90ZV84X2w2Ogp0eG4gU2VuZGVyCmIgYWxsb3dlZHRvdm90ZV84X2w1CmFsbG93ZWR0b3ZvdGVfOF9sNzoKaXR4bl9iZWdpbgpwdXNoaW50IDYgLy8gYXBwbAppdHhuX2ZpZWxkIFR5cGVFbnVtCmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldAppdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKYnl0ZWMgMTYgLy8gIm9wdXAoKXZvaWQiCml0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCmludGNfMCAvLyAwCml0eG5fZmllbGQgRmVlCml0eG5fc3VibWl0CmIgYWxsb3dlZHRvdm90ZV84X2wyCmFsbG93ZWR0b3ZvdGVfOF9sODoKaW50Y18xIC8vIDEKYWxsb3dlZHRvdm90ZV84X2w5OgpyZXRzdWIKCi8vIHZvdGluZ19vcGVuCnZvdGluZ29wZW5fOToKcHJvdG8gMCAxCmJ5dGVjIDUgLy8gImlzX2Jvb3RzdHJhcHBlZCIKYXBwX2dsb2JhbF9nZXQKaW50Y18xIC8vIDEKPT0KYnl0ZWMgNyAvLyAiY2xvc2VfdGltZSIKYXBwX2dsb2JhbF9nZXQKaW50Y18wIC8vIDAKPT0KJiYKZ2xvYmFsIExhdGVzdFRpbWVzdGFtcApieXRlYyAxMiAvLyAic3RhcnRfdGltZSIKYXBwX2dsb2JhbF9nZXQKPj0KJiYKZ2xvYmFsIExhdGVzdFRpbWVzdGFtcApieXRlYyAxMyAvLyAiZW5kX3RpbWUiCmFwcF9nbG9iYWxfZ2V0CjwKJiYKcmV0c3ViCgovLyBhbHJlYWR5X3ZvdGVkCmFscmVhZHl2b3RlZF8xMDoKcHJvdG8gMCAxCmJ5dGVjXzEgLy8gIiIKdHhuIFNlbmRlcgpmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCnB1c2hpbnQgMzIgLy8gMzIKPT0KYXNzZXJ0CmZyYW1lX2RpZyAwCmJveF9sZW4Kc3RvcmUgNjEKc3RvcmUgNjAKbG9hZCA2MQpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfcHJlY29uZGl0aW9ucwpnZXRwcmVjb25kaXRpb25zXzExOgpwcm90byAzIDEKYnl0ZWNfMSAvLyAiIgppbnRjXzAgLy8gMApkdXBuIDUKYnl0ZWNfMSAvLyAiIgpkdXAKY2FsbHN1YiB2b3RpbmdvcGVuXzkKZnJhbWVfYnVyeSAxCmZyYW1lX2RpZyAtMwpleHRyYWN0IDIgMApmcmFtZV9kaWcgLTIKZnJhbWVfZGlnIC0xCmNhbGxzdWIgYWxsb3dlZHRvdm90ZV84CmZyYW1lX2J1cnkgMgpjYWxsc3ViIGFscmVhZHl2b3RlZF8xMApmcmFtZV9idXJ5IDMKZ2xvYmFsIExhdGVzdFRpbWVzdGFtcApmcmFtZV9idXJ5IDQKZnJhbWVfZGlnIDEKaXRvYgpmcmFtZV9kaWcgMgppdG9iCmNvbmNhdApmcmFtZV9kaWcgMwppdG9iCmNvbmNhdApmcmFtZV9kaWcgNAppdG9iCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyB2b3RlCnZvdGVfMTI6CnByb3RvIDYgMApieXRlY18xIC8vICIiCmludGNfMCAvLyAwCmR1cG4gMTEKYnl0ZWNfMSAvLyAiIgpmcmFtZV9kaWcgLTEKdHhuYXMgQXBwbGljYXRpb25zCmJ5dGVjXzIgLy8gIm91YWlkIgphcHBfZ2xvYmFsX2dldAo9PQovLyBPcFVwIGFwcCBJRCBub3QgcGFzc2VkIGluCmFzc2VydApmcmFtZV9kaWcgLTUKZXh0cmFjdCAyIDAKZnJhbWVfZGlnIC00CmZyYW1lX2RpZyAtMQpjYWxsc3ViIGFsbG93ZWR0b3ZvdGVfOAovLyBOb3QgYWxsb3dlZCB0byB2b3RlCmFzc2VydApjYWxsc3ViIHZvdGluZ29wZW5fOQovLyBWb3Rpbmcgbm90IG9wZW4KYXNzZXJ0CmNhbGxzdWIgYWxyZWFkeXZvdGVkXzEwCiEKLy8gQWxyZWFkeSB2b3RlZAphc3NlcnQKYnl0ZWMgNCAvLyAib3B0aW9uX2NvdW50cyIKYXBwX2dsb2JhbF9nZXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmludGNfMCAvLyAwCmV4dHJhY3RfdWludDE2CmZyYW1lX2J1cnkgMQpmcmFtZV9kaWcgMQpzdG9yZSA2MgpmcmFtZV9kaWcgLTMKaW50Y18wIC8vIDAKZXh0cmFjdF91aW50MTYKZnJhbWVfYnVyeSAyCmZyYW1lX2RpZyAyCmxvYWQgNjIKPT0KLy8gTnVtYmVyIG9mIGFuc3dlcnMgaW5jb3JyZWN0CmFzc2VydApieXRlY18wIC8vICJ2b3RlX3R5cGUiCmFwcF9nbG9iYWxfZ2V0CmludGNfMyAvLyAzCj09CmJueiB2b3RlXzEyX2wyMApmcmFtZV9kaWcgLTIKaW50Y18wIC8vIDAKZXh0cmFjdF91aW50MTYKZnJhbWVfYnVyeSA0CmZyYW1lX2RpZyA0CmludGNfMCAvLyAwCj09Ci8vIE51bWJlciBvZiBhbnN3ZXIgd2VpZ2h0cyBzaG91bGQgYmUgMCBzaW5jZSB0aGlzIHZvdGUgZG9lc24ndCB1c2UgcGFydGl0aW9uZWQgd2VpZ2h0aW5nCmFzc2VydAp2b3RlXzEyX2wyOgpwdXNoaW50IDI1MDAgLy8gMjUwMApwdXNoaW50IDM0IC8vIDM0CmludGNfMSAvLyAxCmZyYW1lX2RpZyAtMwppbnRjXzAgLy8gMApleHRyYWN0X3VpbnQxNgpmcmFtZV9idXJ5IDYKZnJhbWVfZGlnIDYKKgorCnB1c2hpbnQgNDAwIC8vIDQwMAoqCisKc3RvcmUgNjMKZnJhbWVfZGlnIC02Cmd0eG5zIFJlY2VpdmVyCmdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCj09Ci8vIFBheW1lbnQgbXVzdCBiZSB0byBhcHAgYWRkcmVzcwphc3NlcnQKbG9hZCA2MwppdG9iCmxvZwpmcmFtZV9kaWcgLTYKZ3R4bnMgQW1vdW50CmxvYWQgNjMKPT0KLy8gUGF5bWVudCBtdXN0IGJlIHRoZSBleGFjdCBtaW4gYmFsYW5jZSByZXF1aXJlbWVudAphc3NlcnQKaW50Y18wIC8vIDAKc3RvcmUgNjQKaW50Y18wIC8vIDAKc3RvcmUgNjUKaW50Y18wIC8vIDAKc3RvcmUgNjYKdm90ZV8xMl9sMzoKbG9hZCA2Ngpsb2FkIDYyCjwKYm56IHZvdGVfMTJfbDYKYnl0ZWNfMCAvLyAidm90ZV90eXBlIgphcHBfZ2xvYmFsX2dldAppbnRjXzMgLy8gMwo9PQpieiB2b3RlXzEyX2wyMQpsb2FkIDY1CmZyYW1lX2RpZyAtNAo9PQovLyBEaWRuJ3QgcGFydGl0aW9uIGV4YWN0IHZvdGluZyB3ZWlnaHQgYWNyb3NzIHF1ZXN0aW9ucwphc3NlcnQKYiB2b3RlXzEyX2wyMQp2b3RlXzEyX2w2OgpnbG9iYWwgT3Bjb2RlQnVkZ2V0CnB1c2hpbnQgMTAwIC8vIDEwMAo8CmJueiB2b3RlXzEyX2wxNwp2b3RlXzEyX2w3OgpmcmFtZV9kaWcgLTMKaW50Y18xIC8vIDEKbG9hZCA2NgoqCnB1c2hpbnQgMiAvLyAyCisKZ2V0Ynl0ZQpmcmFtZV9idXJ5IDcKaW50Y18wIC8vIDAKZnJhbWVfYnVyeSA5CmJ5dGVjXzAgLy8gInZvdGVfdHlwZSIKYXBwX2dsb2JhbF9nZXQKaW50Y18zIC8vIDMKPT0KYm56IHZvdGVfMTJfbDE2CnZvdGVfMTJfbDg6CmZyYW1lX2RpZyAwCmludGNfMSAvLyAxCmxvYWQgNjYKKgpwdXNoaW50IDIgLy8gMgorCmdldGJ5dGUKZnJhbWVfYnVyeSAxMQpmcmFtZV9kaWcgNwpmcmFtZV9kaWcgMTEKPAovLyBBbnN3ZXIgb3B0aW9uIGluZGV4IGludmFsaWQKYXNzZXJ0CnB1c2hpbnQgOCAvLyA4CmxvYWQgNjQKZnJhbWVfZGlnIDcKKwoqCnN0b3JlIDY4CmJ5dGVjIDkgLy8gIlYiCmxvYWQgNjgKcHVzaGludCA4IC8vIDgKYm94X2V4dHJhY3QKYnRvaQpzdG9yZSA2OQpieXRlYyA5IC8vICJWIgpsb2FkIDY4CmxvYWQgNjkKYnl0ZWNfMCAvLyAidm90ZV90eXBlIgphcHBfZ2xvYmFsX2dldAppbnRjXzAgLy8gMAo9PQpieXRlY18wIC8vICJ2b3RlX3R5cGUiCmFwcF9nbG9iYWxfZ2V0CmludGNfMSAvLyAxCj09Cnx8CmJueiB2b3RlXzEyX2wxNQpieXRlY18wIC8vICJ2b3RlX3R5cGUiCmFwcF9nbG9iYWxfZ2V0CnB1c2hpbnQgMiAvLyAyCj09CmJueiB2b3RlXzEyX2wxNApmcmFtZV9kaWcgOQp2b3RlXzEyX2wxMToKKwppdG9iCmJveF9yZXBsYWNlCmxvYWQgNjQKZnJhbWVfZGlnIDExCisKc3RvcmUgNjQKYnl0ZWNfMCAvLyAidm90ZV90eXBlIgphcHBfZ2xvYmFsX2dldAppbnRjXzMgLy8gMwo9PQpibnogdm90ZV8xMl9sMTMKdm90ZV8xMl9sMTI6CmxvYWQgNjYKaW50Y18xIC8vIDEKKwpzdG9yZSA2NgpiIHZvdGVfMTJfbDMKdm90ZV8xMl9sMTM6CmxvYWQgNjUKZnJhbWVfZGlnIDkKKwpzdG9yZSA2NQpiIHZvdGVfMTJfbDEyCnZvdGVfMTJfbDE0OgpmcmFtZV9kaWcgLTQKYiB2b3RlXzEyX2wxMQp2b3RlXzEyX2wxNToKaW50Y18xIC8vIDEKYiB2b3RlXzEyX2wxMQp2b3RlXzEyX2wxNjoKZnJhbWVfZGlnIC0yCnB1c2hpbnQgOCAvLyA4CmxvYWQgNjYKKgpwdXNoaW50IDIgLy8gMgorCmV4dHJhY3RfdWludDY0CmZyYW1lX2J1cnkgOQpiIHZvdGVfMTJfbDgKdm90ZV8xMl9sMTc6CnB1c2hpbnQgNjgwIC8vIDY4MAppbnRjXzIgLy8gMTAKKwpzdG9yZSA2Nwp2b3RlXzEyX2wxODoKbG9hZCA2NwpnbG9iYWwgT3Bjb2RlQnVkZ2V0Cj4KYnogdm90ZV8xMl9sNwppdHhuX2JlZ2luCnB1c2hpbnQgNiAvLyBhcHBsCml0eG5fZmllbGQgVHlwZUVudW0KYnl0ZWNfMiAvLyAib3VhaWQiCmFwcF9nbG9iYWxfZ2V0Cml0eG5fZmllbGQgQXBwbGljYXRpb25JRApieXRlYyAxNiAvLyAib3B1cCgpdm9pZCIKaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKaW50Y18wIC8vIDAKaXR4bl9maWVsZCBGZWUKaXR4bl9zdWJtaXQKYiB2b3RlXzEyX2wxOAp2b3RlXzEyX2wyMDoKZnJhbWVfZGlnIC0yCmludGNfMCAvLyAwCmV4dHJhY3RfdWludDE2CmZyYW1lX2J1cnkgMwpmcmFtZV9kaWcgMwpsb2FkIDYyCj09Ci8vIE51bWJlciBvZiBhbnN3ZXIgd2VpZ2h0cyBpbmNvcnJlY3QsIHNob3VsZCBtYXRjaCBudW1iZXIgb2YgcXVlc3Rpb25zIHNpbmNlIHRoaXMgdm90ZSB1c2VzIHBhcnRpdGlvbmVkIHdlaWdodGluZwphc3NlcnQKYiB2b3RlXzEyX2wyCnZvdGVfMTJfbDIxOgp0eG4gU2VuZGVyCmZyYW1lX2J1cnkgMTMKZnJhbWVfZGlnIDEzCmxlbgpwdXNoaW50IDMyIC8vIDMyCj09CmFzc2VydApmcmFtZV9kaWcgMTMKYm94X2RlbApwb3AKZnJhbWVfZGlnIDEzCmZyYW1lX2RpZyAtMwpib3hfcHV0CmJ5dGVjIDYgLy8gInZvdGVyX2NvdW50IgpieXRlYyA2IC8vICJ2b3Rlcl9jb3VudCIKYXBwX2dsb2JhbF9nZXQKaW50Y18xIC8vIDEKKwphcHBfZ2xvYmFsX3B1dApyZXRzdWI=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu" + }, + "state": { + "global": { + "num_byte_slices": 5, + "num_uints": 10 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": { + "close_time": { + "type": "uint64", + "key": "close_time", + "descr": "The unix timestamp of the time the vote was closed" + }, + "end_time": { + "type": "uint64", + "key": "end_time", + "descr": "The unix timestamp of the ending time of voting" + }, + "is_bootstrapped": { + "type": "uint64", + "key": "is_bootstrapped", + "descr": "Whether or not the contract has been bootstrapped with answers" + }, + "metadata_ipfs_cid": { + "type": "bytes", + "key": "metadata_ipfs_cid", + "descr": "The IPFS content ID of the voting metadata file" + }, + "nft_asset_id": { + "type": "uint64", + "key": "nft_asset_id", + "descr": "The asset ID of a result NFT if one has been created" + }, + "nft_image_url": { + "type": "bytes", + "key": "nft_image_url", + "descr": "The IPFS URL of the default image to use as the media of the result NFT" + }, + "option_counts": { + "type": "bytes", + "key": "option_counts", + "descr": "The number of options for each question" + }, + "opup_app_id": { + "type": "uint64", + "key": "ouaid", + "descr": "" + }, + "quorum": { + "type": "uint64", + "key": "quorum", + "descr": "The minimum number of voters to reach quorum" + }, + "snapshot_public_key": { + "type": "bytes", + "key": "snapshot_public_key", + "descr": "The public key of the Ed25519 compatible private key that was used to encrypt entries in the vote gating snapshot" + }, + "start_time": { + "type": "uint64", + "key": "start_time", + "descr": "The unix timestamp of the starting time of voting" + }, + "total_options": { + "type": "uint64", + "key": "total_options", + "descr": "The total number of options" + }, + "vote_id": { + "type": "bytes", + "key": "vote_id", + "descr": "The identifier of this voting round" + }, + "vote_type": { + "type": "uint64", + "key": "vote_type", + "descr": "The type of this voting round; 0 = no snapshot / weighting, 1 = snapshot & no weighting, 2 = snapshot & weighting per question, 3 = snapshot & weighting partitioned across the questions" + }, + "voter_count": { + "type": "uint64", + "key": "voter_count", + "descr": "The minimum number of voters who have voted" + } + }, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "VotingRoundApp", + "methods": [ + { + "name": "opup_bootstrap", + "args": [ + { + "type": "pay", + "name": "ptxn" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "initialize opup with bootstrap to create a target app" + }, + { + "name": "create", + "args": [ + { + "type": "string", + "name": "vote_id" + }, + { + "type": "uint8", + "name": "vote_type" + }, + { + "type": "byte[]", + "name": "snapshot_public_key" + }, + { + "type": "string", + "name": "metadata_ipfs_cid" + }, + { + "type": "uint64", + "name": "start_time" + }, + { + "type": "uint64", + "name": "end_time" + }, + { + "type": "uint8[]", + "name": "option_counts" + }, + { + "type": "uint64", + "name": "quorum" + }, + { + "type": "string", + "name": "nft_image_url" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "bootstrap", + "args": [ + { + "type": "pay", + "name": "fund_min_bal_req" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "close", + "args": [ + { + "type": "application", + "name": "opup_app" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "get_preconditions", + "args": [ + { + "type": "byte[]", + "name": "signature" + }, + { + "type": "uint64", + "name": "weighting" + }, + { + "type": "application", + "name": "opup_app" + } + ], + "returns": { + "type": "(uint64,uint64,uint64,uint64)" + } + }, + { + "name": "vote", + "args": [ + { + "type": "pay", + "name": "fund_min_bal_req" + }, + { + "type": "byte[]", + "name": "signature" + }, + { + "type": "uint64", + "name": "weighting" + }, + { + "type": "uint8[]", + "name": "answer_ids" + }, + { + "type": "uint64[]", + "name": "answer_weights" + }, + { + "type": "application", + "name": "opup_app" + } + ], + "returns": { + "type": "void" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "delete_application": "CALL" + } +} diff --git a/package-lock.json b/package-lock.json index f550901..ea798df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@commitlint/config-conventional": "^19.1.0", "@makerx/eslint-config": "^3.1.1", "@makerx/prettier-config": "^2.0.1", + "@prisma/client": "^5.15.1", "@rollup/plugin-typescript": "^11.1.6", "@vitest/coverage-v8": "^1.4.0", "better-npm-audit": "^3.7.3", @@ -29,6 +30,7 @@ "eslint": "8.57.0", "npm-run-all": "^4.1.5", "prettier": "3.2.5", + "prisma": "^5.15.1", "rimraf": "^5.0.5", "rollup": "^4.13.0", "semantic-release": "^23.0.6", @@ -43,7 +45,7 @@ "node": ">=18.0" }, "peerDependencies": { - "@algorandfoundation/algokit-utils": "^6.0.0-beta.1", + "@algorandfoundation/algokit-utils": "^6.0.5", "algosdk": "^2.7.0" } }, @@ -57,9 +59,9 @@ } }, "node_modules/@algorandfoundation/algokit-utils": { - "version": "6.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-6.0.0-beta.1.tgz", - "integrity": "sha512-efzQCmiyOs1bHKxdWitrHrUMfPn/K48BzXjm7VBX5dAn592dXUK0oi7LJ5O0ZLkR4+1kHJ3m1Jy+uuU9dFy7tQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-6.0.5.tgz", + "integrity": "sha512-/a4pD62nQ58r6JnuNFZm9KAC9D8M7JNeAYTaIuA4TwmdLeypdUA9SD8BiaLAI8aR/GLaaPEILHnSbRSlPq9GPw==", "peer": true, "dependencies": { "buffer": "^6.0.3" @@ -1725,6 +1727,69 @@ "node": ">=12" } }, + "node_modules/@prisma/client": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.15.1.tgz", + "integrity": "sha512-fmZRGmsUJ9+VwC/AvfP/PwdpD0xAEyPvNsD9/B3+GYpETq9VejVRT3PiqNvl76q1uYYzNZeo8u/LmzzTetHSEg==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.15.1.tgz", + "integrity": "sha512-NQjdEplhXEcPvf84ghxExC+LD+iTimbg3sZvA3BhybVQIocBEBxFf9GTHhmRVPmjrWoBaYJBVgEEBXZT27JTbQ==", + "dev": true + }, + "node_modules/@prisma/engines": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.15.1.tgz", + "integrity": "sha512-1iTRxJEFvpBpEWf2bYiMG6LBBQhX7X+GA5piH+tmPWgc/v+/ElxQf2kjQxby8AErmZqtZkdoKJ7FSRjNjBPE9Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.15.1", + "@prisma/engines-version": "5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3", + "@prisma/fetch-engine": "5.15.1", + "@prisma/get-platform": "5.15.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3.tgz", + "integrity": "sha512-7csphKGCG6n/cN1MkT1mJvQ78Ir18IknlYZ8eyEoLKdQBb0HscR/6TyPmzqrMA7Rz01K1KeXqctwAqxtA/lKQg==", + "dev": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.15.1.tgz", + "integrity": "sha512-mj0wfsJ+mAdDp1ynT2JKxAXa+CoYMT267qF7g2Uv+oaVTI2CMfGWouMARht8T2QLTgl+gpXSFTwIYbcR+oWEtw==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.15.1", + "@prisma/engines-version": "5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3", + "@prisma/get-platform": "5.15.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.15.1.tgz", + "integrity": "sha512-oFccp7bYys+ZYkmtYzjR+0cRrGKvSuF+h5QhSkyEsYQ9kzJzQRvuWt2SiHRPt8xOQ4MTmujM+bP5uOexnnAHdQ==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.15.1" + } + }, "node_modules/@rollup/plugin-typescript": { "version": "11.1.6", "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", @@ -10124,6 +10189,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.15.1.tgz", + "integrity": "sha512-pYsUVpTlYvZ6mWvZKDv9rKdUa7tlfSUJY1CVtgb8Had1pHbIm9fr1MBASccr5XnSuCUrjnvKhWNwgSYy6aCajA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.15.1" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index a69f1af..616e98e 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,13 @@ "pre-commit": "run-s check-types lint:fix audit format test generate:code-docs", "dhm": "ts-node-dev --project tsconfig.dev.json --transpile-only --watch .env -r dotenv/config ./examples/data-history-museum/index.ts", "watch-dhm": "cross-env RUN_LOOP=true npm run dhm", - "xgov": "ts-node-dev --project tsconfig.dev.json --transpile-only --watch .env -r dotenv/config ./examples/xgov-voting/index.ts", + "xgov": "npx prisma migrate dev && ts-node-dev --project tsconfig.dev.json --transpile-only --watch .env -r dotenv/config ./examples/xgov-voting/index.ts", "watch-xgov": "cross-env RUN_LOOP=true npm run xgov", "usdc": "ts-node-dev --project tsconfig.dev.json --transpile-only --watch .env -r dotenv/config ./examples/usdc/index.ts" }, + "prisma": { + "schema": "examples/xgov-voting/prisma/schema.prisma" + }, "author": "Algorand Foundation", "license": "MIT", "engines": { @@ -63,6 +66,7 @@ "./package.json": "./package.json" }, "devDependencies": { + "@prisma/client": "^5.15.1", "@commitlint/cli": "^19.2.1", "@commitlint/config-conventional": "^19.1.0", "@makerx/eslint-config": "^3.1.1", @@ -77,6 +81,7 @@ "eslint": "8.57.0", "npm-run-all": "^4.1.5", "prettier": "3.2.5", + "prisma": "^5.15.1", "rimraf": "^5.0.5", "rollup": "^4.13.0", "semantic-release": "^23.0.6", @@ -94,7 +99,7 @@ "js-sha512": "^0.9.0" }, "peerDependencies": { - "@algorandfoundation/algokit-utils": "^6.0.0-beta.1", + "@algorandfoundation/algokit-utils": "^6.0.5", "algosdk": "^2.7.0" }, "overrides": { diff --git a/src/subscriber.ts b/src/subscriber.ts index f3e5d7b..815778e 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -5,6 +5,7 @@ import { AsyncEventEmitter, AsyncEventListener } from './types/async-event-emitt import type { AlgorandSubscriberConfig, BeforePollMetadata, + ErrorListener, SubscribedTransaction, TransactionSubscriptionResult, TypedAsyncEventListener, @@ -26,6 +27,11 @@ export class AlgorandSubscriber { private startPromise: Promise | undefined private filterNames: string[] + private readonly errorEventName = 'error' + private readonly defaultErrorHandler = (error: unknown) => { + throw error + } + /** * Create a new `AlgorandSubscriber`. * @param config The subscriber configuration @@ -37,7 +43,7 @@ export class AlgorandSubscriber { this.indexer = indexer this.config = config this.abortController = new AbortController() - this.eventEmitter = new AsyncEventEmitter() + this.eventEmitter = new AsyncEventEmitter().on(this.errorEventName, this.defaultErrorHandler) this.filterNames = this.config.filters .map((f) => f.name) @@ -107,6 +113,9 @@ export class AlgorandSubscriber { start(inspect?: (pollResult: TransactionSubscriptionResult) => void, suppressLog?: boolean): void { if (this.started) return this.started = true + if (this.abortController.signal.aborted) { + this.abortController = new AbortController() + } this.startPromise = (async () => { while (!this.abortController.signal.aborted) { // eslint-disable-next-line no-console @@ -115,6 +124,7 @@ export class AlgorandSubscriber { const durationInSeconds = (+new Date() - start) / 1000 algokit.Config.getLogger(suppressLog).debug('Subscription poll', { currentRound: result.currentRound, + startingWatermark: result.startingWatermark, newWatermark: result.newWatermark, syncedRoundRange: result.syncedRoundRange, subscribedTransactionsLength: result.subscribedTransactions.length, @@ -141,6 +151,10 @@ export class AlgorandSubscriber { } this.started = false })() + this.startPromise.catch(async (e) => { + this.started = false + await this.eventEmitter.emitAsync(this.errorEventName, e) + }) } /** Stops the subscriber if previously started via `start`. @@ -175,6 +189,9 @@ export class AlgorandSubscriber { * @returns The subscriber so `on*` calls can be chained */ on(filterName: string, listener: TypedAsyncEventListener) { + if (filterName === this.errorEventName) { + throw new Error(`'${this.errorEventName}' is reserved, please supply a different filterName.`) + } this.eventEmitter.on(filterName, listener as AsyncEventListener) return this } @@ -243,4 +260,38 @@ export class AlgorandSubscriber { this.eventEmitter.on('poll', listener as AsyncEventListener) return this } + + /** + * Register an error handler to run if an error is thrown during processing or event handling. + * + * This is useful to handle any errors that occur and can be used to perform retries, logging or cleanup activities. + * + * The listener can be async and it will be awaited if so. + * @example + * ```typescript + * subscriber.onError((error) => { console.error(error) }) + * ``` + * @example + * ```typescript + * const maxRetries = 3 + * let retryCount = 0 + * subscriber.onError(async (error) => { + * retryCount++ + * if (retryCount > maxRetries) { + * console.error(error) + * return + * } + * console.log(`Error occurred, retrying in 2 seconds (${retryCount}/${maxRetries})`) + * await new Promise((r) => setTimeout(r, 2_000)) + * subscriber.start() + *}) + * ``` + * @param listener The listener function to invoke with the error that was thrown + * @returns The subscriber so `on*` calls can be chained + */ + onError(listener: ErrorListener) { + // Remove the default error handling, as errors are being handled. + this.eventEmitter.off(this.errorEventName, this.defaultErrorHandler).on(this.errorEventName, listener as AsyncEventListener) + return this + } } diff --git a/src/subscriptions.ts b/src/subscriptions.ts index ed7f92f..348fd23 100644 --- a/src/subscriptions.ts +++ b/src/subscriptions.ts @@ -87,6 +87,7 @@ export async function getSubscribedTransactions( if (currentRound <= watermark) { return { currentRound: currentRound, + startingWatermark: watermark, newWatermark: watermark, subscribedTransactions: [], syncedRoundRange: [currentRound, currentRound], @@ -210,6 +211,7 @@ export async function getSubscribedTransactions( return { syncedRoundRange: [startRound, endRound], + startingWatermark: watermark, newWatermark: endRound, currentRound, blockMetadata, diff --git a/src/types/async-event-emitter.ts b/src/types/async-event-emitter.ts index 6802753..3837c1b 100644 --- a/src/types/async-event-emitter.ts +++ b/src/types/async-event-emitter.ts @@ -65,11 +65,11 @@ export class AsyncEventEmitter { if (wrappedListener) { this.listenerWrapperMap.delete(listener) if (this.listenerMap[eventName]?.indexOf(wrappedListener) !== -1) { - this.listenerMap[eventName].slice(this.listenerMap[eventName].indexOf(wrappedListener)) + this.listenerMap[eventName].splice(this.listenerMap[eventName].indexOf(wrappedListener), 1) } } else { if (this.listenerMap[eventName]?.indexOf(listener) !== -1) { - this.listenerMap[eventName].slice(this.listenerMap[eventName].indexOf(listener)) + this.listenerMap[eventName].splice(this.listenerMap[eventName].indexOf(listener), 1) } } diff --git a/src/types/subscription.ts b/src/types/subscription.ts index 121ec9a..10ab0ed 100644 --- a/src/types/subscription.ts +++ b/src/types/subscription.ts @@ -9,6 +9,8 @@ export interface TransactionSubscriptionResult { syncedRoundRange: [startRound: number, endRound: number] /** The current detected tip of the configured Algorand blockchain. */ currentRound: number + /** The watermark value that was retrieved at the start of the subscription poll. */ + startingWatermark: number /** The new watermark value to persist for the next call to * `getSubscribedTransactions` to continue the sync. * Will be equal to `syncedRoundRange[1]`. Only persist this @@ -285,3 +287,5 @@ export interface SubscriberConfigFilter extends NamedTransactionFilter { } export type TypedAsyncEventListener = (event: T, eventName: string | symbol) => Promise | void + +export type ErrorListener = (error: unknown) => Promise | void diff --git a/tests/scenarios/catchup-with-indexer.spec.ts b/tests/scenarios/catchup-with-indexer.spec.ts index 2fd430e..a12ca43 100644 --- a/tests/scenarios/catchup-with-indexer.spec.ts +++ b/tests/scenarios/catchup-with-indexer.spec.ts @@ -30,6 +30,7 @@ describe('Subscribing using catchup-with-indexer', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(0) expect(subscribed.newWatermark).toBe(lastTxnRound) expect(subscribed.syncedRoundRange).toEqual([1, lastTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(1) @@ -61,6 +62,7 @@ describe('Subscribing using catchup-with-indexer', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(initialWatermark) expect(subscribed.newWatermark).toBe(expectedNewWatermark) expect(subscribed.syncedRoundRange).toEqual([initialWatermark + 1, expectedNewWatermark]) expect(subscribed.subscribedTransactions.length).toBe(2) @@ -83,6 +85,7 @@ describe('Subscribing using catchup-with-indexer', () => { ) expect(subscribed.currentRound).toBe(currentRound) + expect(subscribed.startingWatermark).toBe(olderTxnRound - 1) expect(subscribed.newWatermark).toBe(currentRound) expect(subscribed.syncedRoundRange).toEqual([olderTxnRound, currentRound]) expect(subscribed.subscribedTransactions.length).toBe(2) @@ -103,6 +106,7 @@ describe('Subscribing using catchup-with-indexer', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(0) expect(subscribed.newWatermark).toBe(lastTxnRound) expect(subscribed.syncedRoundRange).toEqual([1, lastTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(3) diff --git a/tests/scenarios/fail.spec.ts b/tests/scenarios/fail.spec.ts index 72c193d..edf467b 100644 --- a/tests/scenarios/fail.spec.ts +++ b/tests/scenarios/fail.spec.ts @@ -35,6 +35,7 @@ describe('Subscribing using fail', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(lastTxnRound - 1) expect(subscribed.newWatermark).toBe(lastTxnRound) expect(subscribed.syncedRoundRange).toEqual([lastTxnRound, lastTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(1) diff --git a/tests/scenarios/filters.spec.ts b/tests/scenarios/filters.spec.ts index 9b5d26d..d080e7a 100644 --- a/tests/scenarios/filters.spec.ts +++ b/tests/scenarios/filters.spec.ts @@ -11,7 +11,7 @@ describe('Subscribing using various filters', () => { const beforeAllFixtures: (() => Promise)[] = [] beforeAll(async () => { await hooks.beforeAll() - await beforeAllFixtures.map(async (fixture) => await fixture()) + await Promise.all(beforeAllFixtures.map(async (fixture) => await fixture())) await localnet.context.waitForIndexer() }, 30_000) beforeEach(hooks.beforeEach, 10_000) diff --git a/tests/scenarios/multiple-filters.spec.ts b/tests/scenarios/multiple-filters.spec.ts index 5a0a711..4bdd372 100644 --- a/tests/scenarios/multiple-filters.spec.ts +++ b/tests/scenarios/multiple-filters.spec.ts @@ -36,6 +36,7 @@ describe('Subscribing using multiple filters', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(0) expect(subscribed.newWatermark).toBe(lastTxnRound) expect(subscribed.syncedRoundRange).toEqual([1, lastTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(9) diff --git a/tests/scenarios/skip-sync-newest.spec.ts b/tests/scenarios/skip-sync-newest.spec.ts index 4528b7b..b9dd6e8 100644 --- a/tests/scenarios/skip-sync-newest.spec.ts +++ b/tests/scenarios/skip-sync-newest.spec.ts @@ -21,6 +21,7 @@ describe('Subscribing using skip-sync-newest', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(0) expect(subscribed.newWatermark).toBe(lastTxnRound) expect(subscribed.syncedRoundRange).toEqual([lastTxnRound, lastTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(1) @@ -39,6 +40,7 @@ describe('Subscribing using skip-sync-newest', () => { ) expect(subscribed.currentRound).toBe(currentRound) + expect(subscribed.startingWatermark).toBe(olderTxnRound - 1) expect(subscribed.newWatermark).toBe(currentRound) expect(subscribed.syncedRoundRange).toEqual([currentRound, currentRound]) expect(subscribed.subscribedTransactions.length).toBe(1) @@ -56,6 +58,7 @@ describe('Subscribing using skip-sync-newest', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(0) expect(subscribed.newWatermark).toBe(lastTxnRound) expect(subscribed.syncedRoundRange).toEqual([rounds[1], lastTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(2) diff --git a/tests/scenarios/subscriber.spec.ts b/tests/scenarios/subscriber.spec.ts index a422604..e30ed48 100644 --- a/tests/scenarios/subscriber.spec.ts +++ b/tests/scenarios/subscriber.spec.ts @@ -72,6 +72,7 @@ describe('AlgorandSubscriber', () => { expect(subscribedTxns[0]).toBe(txIds[0]) expect(getWatermark()).toBeGreaterThanOrEqual(lastTxnRound) expect(result.currentRound).toBeGreaterThanOrEqual(lastTxnRound) + expect(result.startingWatermark).toBe(lastTxnRound - 1) expect(result.newWatermark).toBe(result.currentRound) expect(result.syncedRoundRange).toEqual([lastTxnRound, result.currentRound]) expect(result.subscribedTransactions.length).toBe(1) @@ -155,6 +156,7 @@ describe('AlgorandSubscriber', () => { expect(subscribedTxns[3].id).toBe(txIds2[0]) expect(subscribedTxns[4].id).toBe(txIds2[1]) expect(result.currentRound).toBeGreaterThanOrEqual(lastTxnRound) + expect(result.startingWatermark).toBe(firstTxnRound - 1) expect(result.newWatermark).toBe(result.currentRound) expect(getWatermark()).toBeGreaterThanOrEqual(result.currentRound) expect(result.syncedRoundRange).toEqual([firstTxnRound, result.currentRound]) @@ -189,6 +191,7 @@ describe('AlgorandSubscriber', () => { expect(subscribedTxns3[3].id).toBe(txIds23[0]) expect(subscribedTxns3[4].id).toBe(txIds23[1]) expect(result3.currentRound).toBeGreaterThanOrEqual(lastSubscribedRound3) + expect(result3.startingWatermark).toBe(result2.newWatermark) expect(result3.newWatermark).toBe(result3.currentRound) expect(getWatermark()).toBeGreaterThanOrEqual(result3.currentRound) expect(result3.syncedRoundRange).toEqual([result2.newWatermark + 1, result3.currentRound]) @@ -356,4 +359,50 @@ describe('AlgorandSubscriber', () => { ]) await subscriber.stop('TEST') }) + + test('Correctly fires onError method', async () => { + const { algod, testAccount } = localnet.context + const { txns } = await SendXTransactions(2, testAccount, algod) + const initialWatermark = Number(txns[0].confirmation!.confirmedRound!) - 1 + let complete = false + let actualError = undefined + const expectedError = new Error('BOOM') + const { subscriber } = getSubscriber( + { + testAccount: algokit.randomAccount(), + initialWatermark, + configOverrides: { + maxRoundsToSync: 100, + syncBehaviour: 'sync-oldest', + frequencyInSeconds: 1000, + filters: [ + { + name: 'account1', + filter: { + sender: algokit.getSenderAddress(testAccount), + }, + }, + ], + }, + }, + algod, + ) + + subscriber + .on('account1', () => { + throw expectedError + }) + .onError((e) => { + actualError = e + complete = true + }) + + subscriber.start() + + console.log('Waiting for up to 2s until subscriber has polled') + await waitFor(() => complete, 2000) + + expect(actualError).toEqual(expectedError) + await subscriber.stop('TEST') + }) }) diff --git a/tests/scenarios/sync-oldest-start-now.spec.ts b/tests/scenarios/sync-oldest-start-now.spec.ts index eaf99a2..168ac62 100644 --- a/tests/scenarios/sync-oldest-start-now.spec.ts +++ b/tests/scenarios/sync-oldest-start-now.spec.ts @@ -21,6 +21,7 @@ describe('Subscribing using sync-oldest-start-now', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(0) expect(subscribed.newWatermark).toBe(lastTxnRound) expect(subscribed.syncedRoundRange).toEqual([lastTxnRound, lastTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(1) @@ -39,6 +40,7 @@ describe('Subscribing using sync-oldest-start-now', () => { ) expect(subscribed.currentRound).toBe(currentRound) + expect(subscribed.startingWatermark).toBe(olderTxnRound - 1) expect(subscribed.newWatermark).toBe(olderTxnRound) expect(subscribed.syncedRoundRange).toEqual([olderTxnRound, olderTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(1) @@ -61,6 +63,7 @@ describe('Subscribing using sync-oldest-start-now', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(rounds[0] - 1) expect(subscribed.newWatermark).toBe(rounds[1]) expect(subscribed.syncedRoundRange).toEqual([rounds[0], rounds[1]]) expect(subscribed.subscribedTransactions.length).toBe(2) diff --git a/tests/scenarios/sync-oldest.spec.ts b/tests/scenarios/sync-oldest.spec.ts index 70e3ac8..c0734db 100644 --- a/tests/scenarios/sync-oldest.spec.ts +++ b/tests/scenarios/sync-oldest.spec.ts @@ -23,6 +23,7 @@ describe('Subscribing using sync-oldest', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(0) expect(subscribed.newWatermark).toBe(1) expect(subscribed.syncedRoundRange).toEqual([1, 1]) expect(subscribed.subscribedTransactions.length).toBe(0) @@ -40,6 +41,7 @@ describe('Subscribing using sync-oldest', () => { ) expect(subscribed.currentRound).toBe(currentRound) + expect(subscribed.startingWatermark).toBe(olderTxnRound - 1) expect(subscribed.newWatermark).toBe(olderTxnRound) expect(subscribed.syncedRoundRange).toEqual([olderTxnRound, olderTxnRound]) expect(subscribed.subscribedTransactions.length).toBe(1) @@ -57,6 +59,7 @@ describe('Subscribing using sync-oldest', () => { ) expect(subscribed.currentRound).toBe(lastTxnRound) + expect(subscribed.startingWatermark).toBe(rounds[0] - 1) expect(subscribed.newWatermark).toBe(rounds[1]) expect(subscribed.syncedRoundRange).toEqual([rounds[0], rounds[1]]) expect(subscribed.subscribedTransactions.length).toBe(2) diff --git a/typedoc.json b/typedoc.json index 35ee584..778a888 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,7 +1,7 @@ { "$schema": "https://typedoc.org/schema.json", "entryPoints": ["src/index.ts", "src/types/*.ts"], - "exclude": ["src/**/*.spec.ts", "tests/**/*.*"], + "exclude": ["src/**/*.spec.ts", "tests/**/*.*", "examples/**/*.*"], "entryPointStrategy": "expand", "out": "docs/code", "plugin": ["typedoc-plugin-markdown"], @@ -10,5 +10,6 @@ "githubPages": false, "readme": "none", "entryDocument": "README.md", - "gitRevision": "main" + "gitRevision": "main", + "tsconfig": "tsconfig.build.json" }