diff --git a/README.md b/README.md index d43b5d09..f03365dc 100644 --- a/README.md +++ b/README.md @@ -588,6 +588,25 @@ An example - LMDB - implementation is available [here](https://github.com/warp-c A dedicated CLI which eases the process of using main methods of the Warp SDK library has been created. Please refer to [`warp-contracts-cli` npm page](https://www.npmjs.com/package/warp-contracts-cli) for more details. +### Customize `fetch` options + +It is possible to customize `fetch` options using dedicated plugin. In order to change `fetch` options one needs to create an implementation of [WarpPlugin](https://github.com/warp-contracts/warp/blob/main/src/core/WarpPlugin.ts) interface. `process` method will receive following properties: + +```ts +interface FetchRequest { + input: RequestInfo | URL; + init: Partial; +} +``` + +...and it should return updated `fetch` options (by returning updated `init` object). An example of such implementation in [src/tools/fetch-options-plugin.ts](https://github.com/warp-contracts/warp/tree/main/tools/fetch-options-plugin.ts). + +In order to use this plugin, it needs to be attached while creating `Warp` instance, e.g.: + +```ts +const warp = WarpFactory.forMainnet().use(new FetchOptionsPlugin()); +``` + ### Migrations #### old factories to WarpFactory diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index f8f753b5..e6261288 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -36,6 +36,7 @@ import { generateMockVrf } from '../utils/vrf'; import { Signature, SignatureType } from './Signature'; import { ContractDefinition } from '../core/ContractDefinition'; import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator'; +import { WarpFetchWrapper } from '../core/WarpFetchWrapper'; /** * An implementation of {@link Contract} that is backwards compatible with current style @@ -59,6 +60,7 @@ export class HandlerBasedContract implements Contract { private _sorter: InteractionsSorter; private _rootSortKey: string; private signature: Signature; + private warpFetchWrapper: WarpFetchWrapper; constructor( private readonly _contractTxId: string, @@ -113,6 +115,7 @@ export class HandlerBasedContract implements Contract { } this.getCallStack = this.getCallStack.bind(this); + this.warpFetchWrapper = new WarpFetchWrapper(this.warp); } async readState( @@ -301,15 +304,16 @@ export class HandlerBasedContract implements Contract { options.vrf ); - const response = await fetch(`${this._evaluationOptions.sequencerUrl}gateway/sequencer/register`, { - method: 'POST', - body: JSON.stringify(interactionTx), - headers: { - 'Accept-Encoding': 'gzip, deflate, br', - 'Content-Type': 'application/json', - Accept: 'application/json' - } - }) + const response = await this.warpFetchWrapper + .fetch(`${this._evaluationOptions.sequencerUrl}gateway/sequencer/register`, { + method: 'POST', + body: JSON.stringify(interactionTx), + headers: { + 'Accept-Encoding': 'gzip, deflate, br', + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }) .then((res) => { this.logger.debug(res); return res.ok ? res.json() : Promise.reject(res); @@ -746,12 +750,13 @@ export class HandlerBasedContract implements Contract { async syncState(externalUrl: string, params?: any): Promise { const { stateEvaluator } = this.warp; - const response = await fetch( - `${externalUrl}?${new URLSearchParams({ - id: this._contractTxId, - ...params - })}` - ) + const response = await this.warpFetchWrapper + .fetch( + `${externalUrl}?${new URLSearchParams({ + id: this._contractTxId, + ...params + })}` + ) .then((res) => { return res.ok ? res.json() : Promise.reject(res); }) diff --git a/src/contract/deploy/impl/DefaultCreateContract.ts b/src/contract/deploy/impl/DefaultCreateContract.ts index 5e7f8af0..ef2a6a26 100644 --- a/src/contract/deploy/impl/DefaultCreateContract.ts +++ b/src/contract/deploy/impl/DefaultCreateContract.ts @@ -2,11 +2,20 @@ import Arweave from 'arweave'; import Transaction from 'arweave/node/lib/transaction'; import { Signature, SignatureType } from '../../../contract/Signature'; +import { WarpFetchWrapper } from '../../../core/WarpFetchWrapper'; import { SmartWeaveTags } from '../../../core/SmartWeaveTags'; import { Warp } from '../../../core/Warp'; import { WARP_GW_URL } from '../../../core/WarpFactory'; import { LoggerFactory } from '../../../logging/LoggerFactory'; -import { CreateContract, ContractData, ContractDeploy, FromSrcTxContractData, ArWallet, BundlrNodeType, BUNDLR_NODES } from '../CreateContract'; +import { + CreateContract, + ContractData, + ContractDeploy, + FromSrcTxContractData, + ArWallet, + BundlrNodeType, + BUNDLR_NODES +} from '../CreateContract'; import { SourceData, SourceImpl } from './SourceImpl'; import { Buffer } from 'redstone-isomorphic'; @@ -15,10 +24,12 @@ export class DefaultCreateContract implements CreateContract { private readonly source: SourceImpl; private signature: Signature; + private readonly warpFetchWrapper: WarpFetchWrapper; constructor(private readonly arweave: Arweave, private warp: Warp) { this.deployFromSourceTx = this.deployFromSourceTx.bind(this); this.source = new SourceImpl(this.warp); + this.warpFetchWrapper = new WarpFetchWrapper(this.warp); } async deploy(contractData: ContractData, disableBundling?: boolean): Promise { @@ -190,7 +201,7 @@ export class DefaultCreateContract implements CreateContract { }; } - const response = await fetch(`${WARP_GW_URL}/gateway/contracts/deploy`, { + const response = await this.warpFetchWrapper.fetch(`${WARP_GW_URL}/gateway/contracts/deploy`, { method: 'POST', body: JSON.stringify(body), headers: { diff --git a/src/core/WarpFetchWrapper.ts b/src/core/WarpFetchWrapper.ts new file mode 100644 index 00000000..f8777673 --- /dev/null +++ b/src/core/WarpFetchWrapper.ts @@ -0,0 +1,37 @@ +import { LoggerFactory } from '../logging/LoggerFactory'; +import { Warp } from './Warp'; + +export interface FetchRequest { + input: RequestInfo | URL; + init: Partial; +} + +export class WarpFetchWrapper { + private readonly name = 'WarpFetchWrapper'; + private readonly logger = LoggerFactory.INST.create(this.name); + + constructor(private warp: Warp) { + this.warp = warp; + } + + fetch(input: RequestInfo | URL, init?: RequestInit): Promise { + let fetchOptions: RequestInit; + + if (this.warp.hasPlugin('fetch-options')) { + const fetchOptionsPlugin = this.warp.loadPlugin>('fetch-options'); + try { + const updatedFetchOptions = fetchOptionsPlugin.process({ input, init: init || {} }); + fetchOptions = { ...init, ...updatedFetchOptions }; + } catch (e) { + if (e.message) { + this.logger.error(e.message); + } + throw new Error(`Unable to process fetch options: ${e.message}`); + } + } else { + fetchOptions = init; + } + + return fetch(input, fetchOptions); + } +} diff --git a/src/core/WarpPlugin.ts b/src/core/WarpPlugin.ts index e15f9a8d..4eee3bcd 100644 --- a/src/core/WarpPlugin.ts +++ b/src/core/WarpPlugin.ts @@ -4,7 +4,8 @@ export const knownWarpPlugins = [ 'smartweave-extension-ethers', 'subscription', 'ivm-handler-api', - 'evaluation-progress' + 'evaluation-progress', + 'fetch-options' ] as const; export type WarpPluginType = typeof knownWarpPlugins[number]; diff --git a/tools/fetch-options-plugin.ts b/tools/fetch-options-plugin.ts new file mode 100644 index 00000000..d0e26897 --- /dev/null +++ b/tools/fetch-options-plugin.ts @@ -0,0 +1,70 @@ +import { WarpPlugin, WarpPluginType } from '../src/core/WarpPlugin'; +import { FetchRequest } from '../src/core/WarpFetchWrapper'; +import { JWKInterface } from 'arweave/node/lib/wallet'; +import fs from 'fs'; +import path from 'path'; +import { LoggerFactory } from '../src/logging/LoggerFactory'; +import { defaultCacheOptions, WarpFactory } from '../src/core/WarpFactory'; + +class FetchOptionsPlugin implements WarpPlugin { + process(request: FetchRequest): Partial { + const url = request.input; + + let fetchOptions: Partial = {}; + + if (url == `https://d1o5nlqr4okus2.cloudfront.net/gateway/sequencer/register`) { + fetchOptions = { + keepalive: true + }; + } + + return fetchOptions; + } + + type(): WarpPluginType { + return 'fetch-options'; + } +} + +async function main() { + const wallet: JWKInterface = readJSON('./.secrets/jwk.json'); + LoggerFactory.INST.logLevel('debug'); + const logger = LoggerFactory.INST.create('FetchOptionsPlugin'); + + try { + const warp = WarpFactory.forMainnet({ ...defaultCacheOptions, inMemory: true }).use(new FetchOptionsPlugin()); + + const jsContractSrc = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.js'), 'utf8'); + const initialState = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.json'), 'utf8'); + + const { contractTxId } = await warp.createContract.deploy({ + wallet, + initState: initialState, + src: jsContractSrc + }); + + const contract = warp.contract(contractTxId).connect(wallet); + + await contract.writeInteraction({ + function: 'transfer', + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 55555 + }); + + const { cachedValue } = await contract.readState(); + logger.info(`Cached value: ${cachedValue}`); + } catch (e) { + logger.error(e); + } +} + +export function readJSON(path: string): JWKInterface { + const content = fs.readFileSync(path, 'utf-8'); + try { + return JSON.parse(content); + } catch (e) { + throw new Error(`File "${path}" does not contain a valid JSON`); + } +} + +main().catch((e) => console.error(e));