@moveflow/sui-sdk.js
Version:
Typescript SDK for MoveFlow on SUI blockchain
904 lines (840 loc) • 31.9 kB
text/typescript
import { Network, Config, getConfig } from './config'
// import { JsonRpcProvider } from '@mysten/sui.js/dist/cjs/providers/json-rpc-provider'
// import { Connection } from '@mysten/sui.js/dist/cjs/rpc/connection'
// import { PaginationArguments } from '@mysten/sui.js/dist/cjs/client'
// import { TransactionArgument, TransactionBlock } from '@mysten/sui.js/dist/cjs/builder'
// import { SUI_CLOCK_OBJECT_ID } from '@mysten/sui.js/dist/cjs/framework'
import { PaginatedObjectsResponse, SuiTransactionBlockResponse } from '@mysten/sui.js/dist/cjs/client/types'
import {
JsonRpcProvider,
Connection,
PaginationArguments,
TransactionArgument,
TransactionBlock,
SUI_CLOCK_OBJECT_ID
} from '@mysten/sui.js'
/**
* FeatureInfo describes features of a payment stream
*
* pauseable - if the payment stream can be paused by the sender
* senderCloseable - if the payment can be closed by the sender
* recipientModifiable - not used now
*/
export type FeatureInfo = {
pauseable: boolean
senderCloseable: boolean
recipientModifiable: boolean
}
/**
* FeeInfo describes fee info of a payment stream
*
* feeRecipient - whenever a payment is withdrawn from the stream, a fee is paid to the recipient
* feePoint - the denominator of fee point is 10000, i.e, 25 means 0.25%
*/
export type FeeInfo = {
feeRecipient: string
feePoint: number
}
/**
* PauseInfo describes pausing info of a payment stream
*
* paused - if the stream is paused or not
* pausedAt - the time when the stream was paused, the value is unix epoch time in seconds
* accPausedTime - accumulated paused time of multiple pauses. It's reset to 0 once a payment is withdrawn
*/
export type PauseInfo = {
paused: boolean
pausedAt: number
accPausedTime: number
}
/**
* StreamInfo describes a payment stream
*
* Explanation of most fields are omitted as the names are self-explanatory.
* The interval is time duration in seconds.
* The lastWithdrawTime, startTime and stopTime are unix epoch time in seconds.
*/
export type StreamInfo = {
id: string
coinType: string
name: string
remark: string
sender: string
recipient: string
interval: number
ratePerInterval: number
lastWithdrawTime: number
startTime: number
stopTime: number
depositAmount: number
withdrawnAmount: number
remainingAmount: number
closed: boolean
featureInfo: FeatureInfo
feeInfo: FeeInfo
pauseInfo: PauseInfo
balance: number
}
/**
* StreamDirection enum describes the relationship between the payment stream and the users.
*
* OUT - outgoing stream. If a user sends money to the stream, the stream is the outgoing stream to the user.
* IN - incoming stream. If a user receives money from the stream, the stream is the incoming stream to the user.
*/
export enum StreamDirection {
OUT,
IN
}
export type StreamCreationResult = {
streamId: string
senderCap: string
recipientCap: string
}
export type CoinConfig = {
coinType: string
feePoint: number
}
export type PaginatedCoinConfigs = {
coinConfigs: CoinConfig[]
nextCursor: string | null
hasNextPage: boolean
}
export type Paginatedstrings = {
objectIds: string[]
nextCursor: PaginatedObjectsResponse['nextCursor']
hasNextPage: boolean
}
type DynamicFields = {
type: string
fields: Record<string, any>
}
export class Stream {
private _network: Network
private _config: Config
private _rpcProvider: JsonRpcProvider
private readonly SUI: string = '0x2::sui::SUI';
constructor(network: Network) {
this._network = network
this._config = getConfig(network)
const conn = new Connection({ fullnode: this._config.fullNodeUrl })
this._rpcProvider = new JsonRpcProvider(conn)
}
get network(): Network {
return this._network
}
get networkName(): string {
return Network[this._network]
}
/**
* This function builds a TransactionBlock to create a new payment stream
*
* @param coinType the coin type that is used in the payment stream, e.g., 0x2::sui::SUI
*
* @param name the name of the stream, max 1024 characters
* @param remark the remark of the stream, max 1024 characters
* @param sender the payment sender's address, which must be the same as the TransactionBlock signer
* @param recipient the payment receiver's address
* @param depositAmount the initial deposit amount of the coin specified by the coinType parameter
* @param startTime the unix epoch time in seconds, i.e., Date.now() / 1000
* @param stopTime the unix epoch time in seconds
* @param interval the payment interval in seconds, and the deposit is divided evenly among the intervals. Default to 60 seconds
* @param closeable if the stream can be closed by the sender. Default to true
* @param modifiable if the stream can be modified by the recipient. Default to true. Not effective now
* @returns the TransactionBlock which can be signed to create a new payment stream
*/
async createTransaction(
coinType: string,
name: string,
remark: string,
sender: string,
recipient: string,
depositAmount: bigint,
startTime: number,
stopTime: number,
interval = 60,
closeable = true,
modifiable = true
): Promise<TransactionBlock> {
this.ensureValidCoinType(coinType)
if (name.length > 1024) throw new Error('name exceeds the maximum length 1024 characters')
if (remark.length > 1024) throw new Error('remark exceeds the maximum length 1024 characters')
// if (!isValidstring(sender)) throw new Error(`${sender} is not a valid address`)
// if (!isValidstring(recipient)) throw new Error(`${recipient} is not a valid address`)
if (depositAmount < 0) throw new Error(`depositAmount ${depositAmount} is negative`)
this.ensureValidTime(startTime)
this.ensureValidTime(stopTime)
if (stopTime <= startTime) throw new Error(`stopTime ${stopTime} is before startTime ${startTime}`)
if (stopTime <= Date.now() / 1000) throw new Error(`stopTime ${stopTime} is in the past`)
this.ensureValidTime(interval)
const balance = await this._rpcProvider.getBalance({ owner: sender, coinType })
const totalBalance = BigInt(balance.totalBalance)
const lockedBalance = balance.lockedBalance.number ? BigInt(balance.lockedBalance.number) : BigInt(0)
const availableBalance = totalBalance - lockedBalance
if (availableBalance <= depositAmount) {
throw new Error(`the sender ${sender} has not enough balance of ${coinType} to pay the deposit ${depositAmount}`)
}
const txb = new TransactionBlock()
const coin = await this.getCoin(txb, sender, coinType, depositAmount)
txb.moveCall({
target: `${this._config.packageObjectId}::stream::create`,
typeArguments: [coinType],
arguments: [
txb.object(this._config.globalConfigObjectId),
coin,
txb.pure(name),
txb.pure(remark),
txb.pure(recipient),
txb.pure(depositAmount),
txb.pure(startTime),
txb.pure(stopTime),
txb.pure(interval),
txb.pure(closeable),
txb.pure(modifiable),
txb.object(SUI_CLOCK_OBJECT_ID),
],
})
return txb
}
/**
* This function builds a TransactionBlock to extend an existing payment stream
*
* @param coinType the coin type that is used in the payment stream, e.g., 0x2::sui::SUI
* @param sender the payment sender's address, which must be the same as the TransactionBlock signer
* @param senderCap the sender capability object id
* @param streamId the payment stream object id
* @param newStopTime the new stop time of the payment stream
* @returns the TransactionBlock which can be signed to extend an existing payment stream
*/
async extendTransaction(
coinType: string,
sender: string,
senderCap: string,
streamId: string,
newStopTime: number
): Promise<TransactionBlock> {
this.ensureValidCoinType(coinType)
// if (!isValidstring(sender)) throw new Error(`${sender} is not a valid address`)
// if (!isValidSuistring(senderCap)) throw new Error(`${senderCap} is not a valid string`)
// if (!isValidSuistring(streamId)) throw new Error(`${streamId} is not a valid string`)
this.ensureValidTime(newStopTime)
const streamInfo = await this.getStreamById(streamId)
if (newStopTime <= streamInfo.stopTime) {
throw new Error(`newStopTime ${newStopTime} is earlier than the stopTime ${streamInfo.stopTime}`)
}
const txb = new TransactionBlock()
const numOfIntervals = (newStopTime - streamInfo.stopTime) / streamInfo.interval
const depositAmount = BigInt(Math.ceil(streamInfo.ratePerInterval * numOfIntervals / 1000))
const coin = await this.getCoin(txb, sender, coinType, depositAmount)
txb.moveCall({
target: `${this._config.packageObjectId}::stream::extend`,
typeArguments: [coinType],
arguments: [
txb.object(senderCap),
txb.object(streamId),
coin,
txb.pure(newStopTime),
],
})
return txb
}
/**
* This function builds a TransactionBlock to pause an existing payment stream
*
* @param coinType the coin type that is used in the payment stream, e.g., 0x2::sui::SUI
* @param senderCap the sender capability object id
* @param streamId the payment stream object id
* @returns the TransactionBlock which can be signed to pause an existing payment stream
*/
pauseTransaction(
coinType: string,
senderCap: string,
streamId: string
): TransactionBlock {
this.ensureValidCoinType(coinType)
// if (!isValidSuistring(senderCap)) throw new Error(`${senderCap} is not a valid string`)
// if (!isValidSuistring(streamId)) throw new Error(`${streamId} is not a valid string`)
const txb = new TransactionBlock()
txb.moveCall({
target: `${this._config.packageObjectId}::stream::pause`,
typeArguments: [coinType],
arguments: [
txb.object(senderCap),
txb.object(streamId),
txb.object(SUI_CLOCK_OBJECT_ID),
],
})
return txb
}
/**
* This function builds a TransactionBlock to resume a paused payment stream
*
* @param coinType the coin type that is used in the payment stream, e.g., 0x2::sui::SUI
* @param senderCap the sender capability object id
* @param streamId the payment stream object id
* @returns the TransactionBlock which can be signed to resume a paused payment stream
*/
resumeTransaction(
coinType: string,
senderCap: string,
streamId: string
): TransactionBlock {
this.ensureValidCoinType(coinType)
// if (!isValidSuistring(senderCap)) throw new Error(`${senderCap} is not a valid string`)
// if (!isValidSuistring(streamId)) throw new Error(`${streamId} is not a valid string`)
const txb = new TransactionBlock()
txb.moveCall({
target: `${this._config.packageObjectId}::stream::resume`,
typeArguments: [coinType],
arguments: [
txb.object(senderCap),
txb.object(streamId),
txb.object(SUI_CLOCK_OBJECT_ID),
],
})
return txb
}
/**
* This function builds a TransactionBlock to withdraw from a payment stream, whose signer must be the recipient.
*
* @param coinType the coin type that is used in the payment stream, e.g., 0x2::sui::SUI
* @param streamId the payment stream object id
* @returns the TransactionBlock which can be signed to withdraw from a payment stream.
*/
withdrawTransaction(
coinType: string,
streamId: string
): TransactionBlock {
this.ensureValidCoinType(coinType)
// if (!isValidSuistring(streamId)) throw new Error(`${streamId} is not a valid string`)
const txb = new TransactionBlock()
txb.moveCall({
target: `${this._config.packageObjectId}::stream::withdraw`,
typeArguments: [coinType],
arguments: [
txb.object(streamId),
txb.object(SUI_CLOCK_OBJECT_ID),
],
})
return txb
}
/**
* This function builds a TransactionBlock to set a new recipient of a payment stream, whose signer must be the current recipient.
*
* @param coinType the coin type that is used in the payment stream, e.g., 0x2::sui::SUI
* @param streamId the payment stream object id
* @returns the TransactionBlock which can be signed to set a new recipient of the payment stream.
*/
setNewRecipientTransaction(
coinType: string,
streamId: string,
newRecipient: string
): TransactionBlock {
this.ensureValidCoinType(coinType)
// if (!isValidSuistring(streamId)) throw new Error(`${streamId} is not a valid string`)
// if (!isValidstring(newRecipient)) throw new Error(` ${newRecipient} is not a valid address`)
const txb = new TransactionBlock()
txb.moveCall({
target: `${this._config.packageObjectId}::stream::set_new_recipient`,
typeArguments: [coinType],
arguments: [
txb.object(streamId),
txb.object(this._config.globalConfigObjectId),
txb.pure(newRecipient),
],
})
return txb
}
/**
* This function builds a TransactionBlock to close an existing payment stream
*
* @param coinType the coin type that is used in the payment stream, e.g., 0x2::sui::SUI
* @param senderCap the sender capability object id
* @param streamId the payment stream object id
* @returns the TransactionBlock which can be signed to close an existing payment stream
*/
closeTransaction(
coinType: string,
senderCap: string,
streamId: string
): TransactionBlock {
this.ensureValidCoinType(coinType)
// if (!isValidSuistring(senderCap)) throw new Error(`${senderCap} is not a valid string`)
// if (!isValidSuistring(streamId)) throw new Error(`${streamId} is not a valid string`)
const txb = new TransactionBlock()
txb.moveCall({
target: `${this._config.packageObjectId}::stream::close`,
typeArguments: [coinType],
arguments: [
txb.object(senderCap),
txb.object(streamId),
txb.object(SUI_CLOCK_OBJECT_ID),
],
})
return txb
}
/**
* This function parses a createTransaction response
*
* @param response the transaction response of the createTransaction
* @returns a StreamCreationResult struct
*/
getStreamCreationResult(response: SuiTransactionBlockResponse): StreamCreationResult {
if (!response.objectChanges) throw new Error('the response is missing object changes')
let streamId = '', senderCap = '', recipientCap = ''
for (const objectChange of response.objectChanges) {
if ('objectType' in objectChange) {
if (objectChange.objectType.includes('StreamInfo'))
streamId = objectChange.objectId
else if (objectChange.objectType.includes('SenderCap'))
senderCap = objectChange.objectId
else if (objectChange.objectType.includes('RecipientCap'))
recipientCap = objectChange.objectId
}
}
const streamCreationResult: StreamCreationResult = {
streamId,
senderCap,
recipientCap,
}
return streamCreationResult
}
/**
* This function get the payment streams specified by the parameters
*
* @param address the sender or recipient's address of the payment streams, depending on the direction
* @param direction the direction of the payment stream, IN or OUT. If IN, then the address is the recipient, otherwise, sender
* @returns an array of StreamInfo objects satisfying the parameters
*/
async getStreams(address: string, direction: StreamDirection): Promise<StreamInfo[]> {
// if (!isValidstring(address)) throw new Error(`${address} is not a valid address`)
const parentId = direction == StreamDirection.IN ? this._config.incomingStreamObjectId : this._config.outgoingStreamObjectId
const streamIds = await this._rpcProvider.getDynamicFieldObject({
parentId,
name: { type: 'address', value: address },
})
const streamIdsContent = streamIds.data?.content as DynamicFields
const streamRecords = await this._rpcProvider.multiGetObjects({
ids: streamIdsContent.fields.value,
options: { showContent: true, showOwner: true },
})
const streams: StreamInfo[] = []
for (let i = 0; i < streamRecords.length; i++) {
const streamRecord = streamRecords[i].data?.content as DynamicFields
if (
direction == StreamDirection.IN && streamRecord.fields.recipient == address ||
direction == StreamDirection.OUT && streamRecord.fields.sender == address
) {
streams.push(this.convert(streamRecord))
}
}
return streams
}
/**
* This function get the stream info specified by the id
*
* @param id the stream id
* @returns the StreamInfo object
*/
async getStreamById(id: string): Promise<StreamInfo> {
// if (!isValidSuistring(id)) throw new Error(`${id} is not a valid string`)
const _record = await this._rpcProvider.getObject({
id,
options: { showContent: true, showOwner: true },
})
const streamInfo = this.convert(_record.data?.content as DynamicFields)
return streamInfo
}
/**
* This function returns the amount of the withdrawable fund in the payment stream
*
* @param stream the StreamInfo object
* @returns the amount of the withdrawable fund
*/
withdrawable(stream: StreamInfo): bigint {
if (stream.closed || stream.pauseInfo.paused || stream.remainingAmount == 0) {
return BigInt(0)
}
const lastWithdrawTime = stream.lastWithdrawTime
const stopTime = stream.stopTime
const interval = stream.interval
const accPausedTime = stream.pauseInfo.accPausedTime
const currTime = Date.now() / 1000 // seconds
const timeSpan = Math.min(currTime, stopTime) - lastWithdrawTime - accPausedTime
const numOfIntervals = Math.floor(timeSpan / interval)
const gross = BigInt(numOfIntervals) * BigInt(stream.ratePerInterval) / BigInt(1000)
const fee = gross * BigInt(stream.feeInfo.feePoint) / BigInt(10000)
return gross - fee
}
/**
* This function returns true if the stream can be closed by the sender, and false otherwise
*
* @param stream the StreamInfo object
* @param sender the sender address
* @returns true if the stream can be closed by the sender, and false otherwise
*/
closeable(stream: StreamInfo, sender: string): boolean {
return stream.sender == sender
&& stream.featureInfo.senderCloseable
&& !stream.closed
}
/**
* This function returns true if the stream can be paused by the sender, and false otherwise
*
* @param stream the StreamInfo object
* @param sender the sender address
* @returns true if the stream can be paused by the sender, and false otherwise
*/
pauseable(stream: StreamInfo, sender: string): boolean {
return stream.sender == sender
&& stream.featureInfo.pauseable
&& !stream.closed
&& !stream.pauseInfo.paused
}
/**
* This function returns true if the stream can be modified by the recipient, and false otherwise
*
* @param stream the StreamInfo object
* @param recipient the recipient address
* @returns true if the stream can be modified by the recipient, and false otherwise
*/
recipientModifiable(stream: StreamInfo, recipient: string): boolean {
return stream.recipient == recipient
&& stream.featureInfo.recipientModifiable
&& !stream.closed
}
/**
* This function returns all the SenderCap object ids of the owner. Use this function if there aren't many (over 100) SenderCaps
*
* @param owner the owner of the SenderCaps
* @returns an array of SenderCap object ids
*/
async getSenderCaps(owner: string): Promise<string[]> {
return this.getOwnedObjects(owner, `${this._config.packageObjectId}::stream::SenderCap`)
}
// async getSenderCap(owner: string) {
// const senderObjects = await this._rpcProvider.getOwnedObjects({
// owner: owner,
// options: {showContent: true},
// filter: {
// StructType: `${this._config.packageObjectId}::stream::SenderCap`
// }
// })
// return senderObjects
// }
/**
* This function returns Paginatedstrings. Use this function if there are many (over 100) senderCaps
*
* @param owner the owner of the SenderCaps
* @param paginationArguments pagination arguments, such as cursor, limit
* @returns Paginatedstrings object
*/
async getPaginatedSenderCaps(
owner: string,
paginationArguments: PaginationArguments<PaginatedObjectsResponse['nextCursor']>
): Promise<Paginatedstrings> {
return this.getPaginatedOwnedObjects(
owner,
`${this._config.packageObjectId}::stream::SenderCap`,
paginationArguments
)
}
/**
* This function returns all the RecipientCap object ids of the owner. Use this function if there aren't many (over 100) RecipientCaps
*
* @param owner the owner of the RecipientCaps
* @returns an array of RecipientCap object ids
*/
async getRecipientCaps(owner: string): Promise<string[]> {
return this.getOwnedObjects(owner, `${this._config.packageObjectId}::stream::RecipientCap`)
}
/**
* This function returns Paginatedstrings. Use this function if there are many (over 100) recipientCaps
*
* @param owner the owner of the RecipientCaps
* @param paginationArguments pagination arguments, such as cursor, limit
* @returns Paginatedstrings object
*/
async getPaginatedRecipientCaps(
owner: string,
paginationArguments: PaginationArguments<PaginatedObjectsResponse['nextCursor']>
): Promise<Paginatedstrings> {
return this.getPaginatedOwnedObjects(
owner,
`${this._config.packageObjectId}::stream::RecipientCap`,
paginationArguments
)
}
/**
* This function returns an array of all the supported coins. Use this function if there aren't many (over 100) coins supported.
*
* @returns all the supported coins
*/
async getSupportedCoins(): Promise<CoinConfig[]> {
const coinConfigs: CoinConfig[] = []
let hasNextPage = true
let nextCursor = null
while (hasNextPage) {
const paginatedCoinConfigs = await this.getPaginatedSupportedCoins({
cursor: nextCursor,
})
coinConfigs.push(...paginatedCoinConfigs.coinConfigs)
hasNextPage = paginatedCoinConfigs.hasNextPage
nextCursor = paginatedCoinConfigs.nextCursor
}
return coinConfigs
}
/**
* This function returns PaginatedCoinConfigs. Use this function if there are many (over 100) coins supported.
*
* @returns paginated supported coins
*/
async getPaginatedSupportedCoins(
paginationArguments: PaginationArguments<string | null>
): Promise<PaginatedCoinConfigs> {
const coinConfigs: CoinConfig[] = []
const coinConfigsObject = await this._rpcProvider.getDynamicFields({
parentId: this._config.coinConfigsObjectId,
...paginationArguments,
})
const objectIds: string[] = []
for (const data of coinConfigsObject.data) {
objectIds.push(data.objectId)
}
const coinConfigObjects = await this._rpcProvider.multiGetObjects({
ids: objectIds,
options: { showContent: true },
})
for (const coinConfigObject of coinConfigObjects) {
const content = coinConfigObject.data?.content as DynamicFields
coinConfigs.push({
coinType: content.fields.value.fields.coin_type,
feePoint: content.fields.value.fields.fee_point,
})
}
return {
coinConfigs,
nextCursor: coinConfigsObject.nextCursor,
hasNextPage: coinConfigsObject.hasNextPage,
}
}
// ---- private functions ----
private async getOwnedObjects(owner: string, structType: string): Promise<string[]> {
// if (!isValidstring(owner)) throw new Error(`${owner} is not a valid address`)
const objectIds: string[] = []
let hasNextPage = true
let nextCursor = null
while (hasNextPage) {
const paginatedOwnedObjects = await this.getPaginatedOwnedObjects(
owner,
structType,
{ cursor: nextCursor }
)
objectIds.push(...paginatedOwnedObjects.objectIds)
hasNextPage = paginatedOwnedObjects.hasNextPage
nextCursor = paginatedOwnedObjects.nextCursor
}
return objectIds
}
private async getPaginatedOwnedObjects(
owner: string,
structType: string,
paginationArguments: PaginationArguments<PaginatedObjectsResponse['nextCursor']>
): Promise<Paginatedstrings> {
// if (!isValidstring(owner)) throw new Error(`${owner} is not a valid address`)
const objectIds: string[] = []
const ownedObjects = await this._rpcProvider.getOwnedObjects({
owner,
filter: {
StructType: structType,
},
...paginationArguments,
})
for (const ownedObject of ownedObjects.data) {
if (ownedObject.data) {
objectIds.push(ownedObject.data.objectId)
}
}
return {
objectIds,
nextCursor: ownedObjects.nextCursor,
hasNextPage: ownedObjects.hasNextPage,
}
}
private convert(streamRecord: DynamicFields): StreamInfo {
const streamInfo: StreamInfo = {
id: streamRecord.fields.id.id,
coinType: this.extractCoinType(streamRecord.type),
name: streamRecord.fields.name,
remark: streamRecord.fields.remark,
sender: streamRecord.fields.sender,
recipient: streamRecord.fields.recipient,
interval: parseInt(streamRecord.fields.interval),
ratePerInterval: parseInt(streamRecord.fields.rate_per_interval),
lastWithdrawTime: parseInt(streamRecord.fields.last_withdraw_time),
startTime: parseInt(streamRecord.fields.start_time),
stopTime: parseInt(streamRecord.fields.stop_time),
depositAmount: parseInt(streamRecord.fields.deposit_amount),
withdrawnAmount: parseInt(streamRecord.fields.withdrawn_amount),
remainingAmount: parseInt(streamRecord.fields.remaining_amount),
closed: streamRecord.fields.closed,
featureInfo: {
pauseable: streamRecord.fields.feature_info.fields.pauseable,
senderCloseable: streamRecord.fields.feature_info.fields.sender_closeable,
recipientModifiable: streamRecord.fields.feature_info.fields.recipient_modifiable,
},
feeInfo: {
feeRecipient: streamRecord.fields.fee_info.fields.fee_recipient,
feePoint: streamRecord.fields.fee_info.fields.fee_point,
},
pauseInfo: {
paused: streamRecord.fields.pause_info.fields.paused,
pausedAt: parseInt(streamRecord.fields.pause_info.fields.pause_at),
accPausedTime: parseInt(streamRecord.fields.pause_info.fields.acc_paused_time),
},
balance: parseInt(streamRecord.fields.balance),
}
return streamInfo
}
private extractCoinType(type: string): string {
const match = type.match(/.+<(.+)>/)
if (!match) throw new Error(`${type} is missing coin type`)
return match[1]
}
private ensurePositiveInteger(num: number) {
if (num < 0 || !Number.isInteger(num)) {
throw new Error(`The number ${num} is negative or not an integer`)
}
}
private ensureValidTime(time: number) {
this.ensurePositiveInteger(time)
if (time > 253402300799) { // 9999/12/31 23:59:59
throw new Error(`The time ${time} is later than 9999/12/31 23:59:59`)
}
}
private ensureValidFeePoint(feePoint: number) {
this.ensurePositiveInteger(feePoint)
if (feePoint > 255) {
throw new Error(`The feePoint ${feePoint} exceeds 255`)
}
}
// basic format validation
private ensureValidCoinType(coinType: string) {
const parts = coinType.split('::')
if (parts.length != 3) throw new Error(`${coinType} is not in a valid format`)
}
private isSUI(coinType: string): boolean {
return coinType === this.SUI
}
private async getCoins(owner: string, coinType: string, amount: bigint): Promise<string[]> {
const coinstrings: string[] = []
let total = BigInt(0)
let hasNextPage = true
let nextCursor = null
while (hasNextPage && total < amount) {
const paginatedCoins = await this._rpcProvider.getCoins({ owner, coinType, cursor: nextCursor })
hasNextPage = paginatedCoins.hasNextPage
nextCursor = paginatedCoins.nextCursor
for (const coin of paginatedCoins.data) {
// if (!coin.lockedUntilEpoch) {
total += BigInt(coin.balance)
coinstrings.push(coin.coinObjectId)
if (total >= amount) break
// }
}
}
return coinstrings
}
private async getCoin(
txb: TransactionBlock,
owner: string,
coinType: string,
amount: bigint
): Promise<TransactionArgument> {
let coin: TransactionArgument
if (this.isSUI(coinType)) {
coin = txb.splitCoins(txb.gas, [txb.pure(amount)])[0]
} else {
const coinstrings = await this.getCoins(owner, coinType, amount)
if (coinstrings.length == 1) {
coin = txb.object(coinstrings[0])
} else { // number of coins cannot be 0, as we have checked the balance before
const primaryCoinInput = txb.object(coinstrings[0])
txb.mergeCoins(primaryCoinInput, coinstrings.slice(1).map(id => txb.object(id)))
coin = txb.splitCoins(primaryCoinInput, [txb.pure(amount)])[0]
}
}
return coin
}
// ----- admin functions -----
/**
* This function builds a TransactionBlock to register a coin type
*
* @param coinType the coin type that is used in the payment streams, e.g., 0x2::sui::SUI
* @param feePoint the denominator of fee point is 10000, i.e, 25 means 0.25%
* @returns the TransactionBlock which can be signed by the admin to register a new coin type
*/
registerCoinTransaction(
coinType: string,
feePoint: number
): TransactionBlock {
this.ensureValidCoinType(coinType)
this.ensureValidFeePoint(feePoint)
const txb = new TransactionBlock()
txb.moveCall({
target: `${this._config.packageObjectId}::stream::register_coin`,
typeArguments: [coinType],
arguments: [
txb.object(this._config.manageCap),
txb.object(this._config.globalConfigObjectId),
txb.pure(feePoint),
],
})
return txb
}
/**
* This function builds a TransactionBlock to set a new fee point for the coin type
*
* @param coinType the coin type that is used in the payment streams, e.g., 0x2::sui::SUI
* @param newFeePoint the denominator of fee point is 10000, i.e, 25 means 0.25%
* @returns the TransactionBlock which can be signed by the admin to set the new fee point
*/
setFeePointTransaction(
coinType: string,
newFeePoint: number
): TransactionBlock {
this.ensureValidCoinType(coinType)
this.ensureValidFeePoint(newFeePoint)
const txb = new TransactionBlock()
txb.moveCall({
target: `${this._config.packageObjectId}::stream::set_fee_point`,
typeArguments: [coinType],
arguments: [
txb.object(this._config.manageCap),
txb.object(this._config.globalConfigObjectId),
txb.pure(newFeePoint),
],
})
return txb
}
/**
* This function builds a TransactionBlock to set a new fee recipient
*
* @param newFeeRecipient the new fee recipient address
* @returns the TransactionBlock which can be signed by the admin to set the new fee recipient
*/
setFeeRecipientTransaction(
newFeeRecipient: string
): TransactionBlock {
// if (!isValidstring(newFeeRecipient)) throw new Error(`${newFeeRecipient} is not a valid address`)
const txb = new TransactionBlock()
txb.moveCall({
target: `${this._config.packageObjectId}::stream::set_fee_recipient`,
arguments: [
txb.object(this._config.manageCap),
txb.object(this._config.globalConfigObjectId),
txb.pure(newFeeRecipient),
],
})
return txb
}
}