-
-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add initial methods #3
Changes from 14 commits
89290d2
d4f9ced
1917dcf
e4f2d9e
6dd2986
5e540a3
af08070
e69a30b
d8f30b5
ea61b00
fdfb49b
9b35e7c
03a5c53
166df21
5349c13
61004e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,25 +5,83 @@ import { | |
NetworkState, | ||
util, | ||
} from '@metamask/controllers'; | ||
import { | ||
APIType, | ||
SmartTransaction, | ||
SignedTransaction, | ||
SignedCanceledTransaction, | ||
UnsignedTransaction, | ||
} from './types'; | ||
import { getAPIRequestURL, isSmartTransactionPending } from './utils'; | ||
|
||
const { handleFetch, safelyExecute } = util; | ||
|
||
// TODO: JSDoc all methods | ||
// TODO: Remove all comments (* ! ?) | ||
|
||
export const DEFAULT_INTERVAL = 5 * 60 * 1000; | ||
|
||
export interface SmartTransactionsConfig extends BaseConfig { | ||
export interface SmartTransactionsControllerConfig extends BaseConfig { | ||
interval: number; | ||
clientId: string; | ||
chainId: string; | ||
allowedNetworks: string[]; | ||
supportedChainIds: string[]; | ||
} | ||
|
||
export interface SmartTransactionsState extends BaseState { | ||
smartTransactions: Record<string, any>; | ||
export interface SmartTransactionsControllerState extends BaseState { | ||
smartTransactions: Record<string, SmartTransaction[]>; | ||
userOptIn: boolean | undefined; | ||
} | ||
|
||
export default class SmartTransactionsController extends BaseController< | ||
SmartTransactionsConfig, | ||
SmartTransactionsState | ||
SmartTransactionsControllerConfig, | ||
SmartTransactionsControllerState | ||
> { | ||
private handle?: NodeJS.Timeout; | ||
private timeoutHandle?: NodeJS.Timeout; | ||
|
||
private updateSmartTransaction(smartTransaction: SmartTransaction): void { | ||
const { chainId } = this.config; | ||
const currentIndex = this.state.smartTransactions[chainId]?.findIndex( | ||
(st) => st.UUID === smartTransaction.UUID, | ||
); | ||
if (currentIndex === -1) { | ||
this.update({ | ||
smartTransactions: { | ||
...this.state.smartTransactions, | ||
[chainId]: [ | ||
...this.state.smartTransactions?.[chainId], | ||
smartTransaction, | ||
], | ||
}, | ||
}); | ||
} else { | ||
this.update({ | ||
smartTransactions: { | ||
...this.state.smartTransactions, | ||
[chainId]: this.state.smartTransactions?.[chainId].map( | ||
(item, index) => { | ||
return index === currentIndex ? smartTransaction : item; | ||
}, | ||
), | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
/* istanbul ignore next */ | ||
private async fetch(request: string, options?: RequestInit) { | ||
const { clientId } = this.config; | ||
const fetchOptions = { | ||
...options, | ||
headers: clientId | ||
? { | ||
'X-Client-Id': clientId, | ||
} | ||
: undefined, | ||
}; | ||
|
||
return handleFetch(request, fetchOptions); | ||
} | ||
|
||
constructor( | ||
{ | ||
|
@@ -33,51 +91,150 @@ export default class SmartTransactionsController extends BaseController< | |
listener: (networkState: NetworkState) => void, | ||
) => void; | ||
}, | ||
config?: Partial<SmartTransactionsConfig>, | ||
state?: Partial<SmartTransactionsState>, | ||
config?: Partial<SmartTransactionsControllerConfig>, | ||
state?: Partial<SmartTransactionsControllerState>, | ||
) { | ||
super(config, state); | ||
|
||
this.defaultConfig = { | ||
interval: DEFAULT_INTERVAL, | ||
chainId: '', | ||
allowedNetworks: ['1'], | ||
clientId: 'default', | ||
supportedChainIds: ['1'], | ||
}; | ||
|
||
this.defaultState = { | ||
smartTransactions: {}, | ||
userOptIn: undefined, | ||
}; | ||
|
||
this.initialize(); | ||
|
||
onNetworkStateChange(({ provider }) => { | ||
const { chainId } = provider; | ||
this.configure({ chainId }); | ||
this.poll(); | ||
}); | ||
this.poll(); | ||
} | ||
|
||
setOptInState(state: boolean | undefined): void { | ||
this.update({ userOptIn: state }); | ||
this.poll(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we have more than 1 polling here, do we want to be more specific? E.g. |
||
} | ||
|
||
async poll(interval?: number): Promise<void> { | ||
const { chainId, allowedNetworks } = this.config; | ||
const { chainId, supportedChainIds } = this.config; | ||
interval && this.configure({ interval }, false, false); | ||
this.handle && clearTimeout(this.handle); | ||
if (!allowedNetworks.includes(chainId)) { | ||
this.timeoutHandle && clearTimeout(this.timeoutHandle); | ||
if (!supportedChainIds.includes(chainId)) { | ||
return; | ||
} | ||
await util.safelyExecute(() => this.updateSmartTransactions()); | ||
this.handle = setTimeout(() => { | ||
await safelyExecute(() => this.updateSmartTransactions()); | ||
this.timeoutHandle = setTimeout(() => { | ||
this.poll(this.config.interval); | ||
}, this.config.interval); | ||
} | ||
|
||
async stop() { | ||
this.handle && clearTimeout(this.handle); | ||
this.timeoutHandle && clearTimeout(this.timeoutHandle); | ||
} | ||
|
||
setOptInState(state: boolean | undefined): void { | ||
this.update({ userOptIn: state }); | ||
} | ||
|
||
async updateSmartTransactions() { | ||
// | ||
const { smartTransactions } = this.state; | ||
const { chainId } = this.config; | ||
|
||
const transactionsToUpdate: string[] = []; | ||
smartTransactions[chainId]?.forEach((smartTransaction) => { | ||
if (isSmartTransactionPending(smartTransaction)) { | ||
transactionsToUpdate.push(smartTransaction.UUID); | ||
} | ||
}); | ||
|
||
if (transactionsToUpdate.length > 0) { | ||
this.fetchSmartTransactionsStatus(transactionsToUpdate); | ||
} else { | ||
this.stop(); | ||
} | ||
} | ||
|
||
// ! Ask backend API to accept list of UUIDs as params | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
async fetchSmartTransactionsStatus(UUIDS: string[]): Promise<void> { | ||
const { chainId } = this.config; | ||
|
||
const params = new URLSearchParams({ | ||
uuids: UUIDS.join(','), | ||
}); | ||
|
||
const url = `${getAPIRequestURL( | ||
APIType.STATUS, | ||
chainId, | ||
)}?${params.toString()}`; | ||
|
||
const data: SmartTransaction[] = await this.fetch(url); | ||
|
||
data.forEach((smartTransaction) => { | ||
this.updateSmartTransaction(smartTransaction); | ||
}); | ||
} | ||
|
||
async getUnsignedTransactionsAndEstimates( | ||
unsignedTransaction: UnsignedTransaction, | ||
): Promise<{ | ||
transactions: UnsignedTransaction[]; | ||
cancelTransactions: UnsignedTransaction[]; | ||
estimates: { | ||
maxFee: number; // GWEI number | ||
estimatedFee: number; // GWEI number | ||
}; | ||
}> { | ||
const { chainId } = this.config; | ||
|
||
const data = await this.fetch( | ||
getAPIRequestURL(APIType.GET_TRANSACTIONS, chainId), | ||
{ | ||
method: 'POST', | ||
body: JSON.stringify({ tx: unsignedTransaction }), | ||
}, | ||
); | ||
|
||
return data; | ||
} | ||
|
||
// * After this successful call client must add a nonce representative to | ||
// * transaction controller external transactions list | ||
async submitSignedTransactions({ | ||
signedTransactions, | ||
signedCanceledTransactions, | ||
}: { | ||
signedTransactions: SignedTransaction[]; | ||
signedCanceledTransactions: SignedCanceledTransaction[]; | ||
}) { | ||
const { chainId } = this.config; | ||
const data = await this.fetch( | ||
getAPIRequestURL(APIType.SUBMIT_TRANSACTIONS, chainId), | ||
{ | ||
method: 'POST', | ||
body: JSON.stringify({ | ||
signedTransactions, | ||
// TODO: Check if canceled transactions can be part of signedTransactions. | ||
signedCanceledTransactions, | ||
}), | ||
}, | ||
); | ||
|
||
this.updateSmartTransaction({ UUID: data.uuid }); | ||
} | ||
|
||
// ! This should return if the cancellation was on chain or not (for nonce management) | ||
// * After this successful call client must update nonce representative | ||
// * in transaction controller external transactions list | ||
// ! Ask backend API to make this endpoint a POST | ||
async cancelSmartTransaction(UUID: string): Promise<void> { | ||
const { chainId } = this.config; | ||
await this.fetch(getAPIRequestURL(APIType.CANCEL, chainId), { | ||
method: 'POST', | ||
body: JSON.stringify({ uuid: UUID }), | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const API_BASE_URL = 'https://api2.metaswap-st.codefi.network'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/** API */ | ||
|
||
export enum APIType { | ||
'GET_TRANSACTIONS', | ||
'SUBMIT_TRANSACTIONS', | ||
'CANCEL', | ||
'STATUS', | ||
'LIVENESS', | ||
} | ||
|
||
/** SmartTransactions */ | ||
|
||
export enum SmartTransactionMinedTx { | ||
NOT_MINED = 'not_mined', | ||
SUCCESS = 'success', | ||
CANCELLED = 'cancelled', | ||
REVERTED = 'reverted', | ||
UNKNOWN = 'unknown', | ||
} | ||
|
||
export enum SmartTransactionCancellationReason { | ||
NOT_CANCELLED = 'not_cancelled', | ||
} | ||
|
||
export interface SmartTransactionsStatus { | ||
error?: string; | ||
cancellationFeeWei: number; | ||
cancellationReason: SmartTransactionCancellationReason; | ||
deadlineRatio: number; | ||
minedHash: string | undefined; | ||
minedTx: SmartTransactionMinedTx; | ||
} | ||
|
||
export interface SmartTransaction { | ||
UUID: string; | ||
status?: SmartTransactionsStatus; | ||
} | ||
|
||
// TODO: maybe grab the type from transactions controller? | ||
export type UnsignedTransaction = any; | ||
|
||
// TODO | ||
export type SignedTransaction = any; | ||
|
||
// TODO | ||
export type SignedCanceledTransaction = any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1