Matching System Part3: Asset 資產模組

https://raw.githubusercontent.com/superj80820/system-design/master/doc/asset.jpg

source code:


用戶資產除了基本的UserID、AssetID、Available(可用資產),還需Frozen(凍結資產)欄位,以實現用戶下單時把下單資金凍結。

type UserAsset struct {
	UserID    int
	AssetID   int
	Available decimal.Decimal
	Frozen    decimal.Decimal
}

一個交易對會有多個用戶,並且每個用戶都會有多個資產,如果資產都需存在memory當中,可以用兩個hash table來實作asset repository。

由於在撮合時也有機會透過API讀取memory的用戶資產,所以須用read-write-lock保護。

type assetRepo struct {
	usersAssetsMap map[int]map[int]*domain.UserAsset
	lock           *sync.RWMutex

	// another code...
}

用戶資產的use case主要有轉帳Transfer()、凍結Freeze()、解凍Unfreeze(),另外我們還需liability user(負債帳戶),他代表整個系統實際的資產,也可用來對帳,負債帳戶透過LiabilityUserTransfer()將資產從系統轉給用戶。

type UserAssetUseCase interface {
	LiabilityUserTransfer(ctx context.Context, toUserID, assetID int, amount decimal.Decimal) (*TransferResult, error)

	TransferFrozenToAvailable(ctx context.Context, fromUserID, toUserID int, assetID int, amount decimal.Decimal) (*TransferResult, error)
	TransferAvailableToAvailable(ctx context.Context, fromUserID, toUserID int, assetID int, amount decimal.Decimal) (*TransferResult, error)
	Freeze(ctx context.Context, userID, assetID int, amount decimal.Decimal) (*TransferResult, error)
	Unfreeze(ctx context.Context, userID, assetID int, amount decimal.Decimal) (*TransferResult, error)

	// ..another functions
}

如果liability user id為1、用戶A user id為100、btc id為1、usdt id為2,其他用戶在系統已存入10btc與100000usdt。

此時用戶A對系統deposit儲值100000 usdt,並下了1顆btc價格為62000usdt的買單,此時需凍結62000usdt,將資產直接展開成一個二維表顯示如下:

user idasset idavailablefrozen
11-100
12-2000000
100100
10023800062000
其他用戶…

在deposit時會呼叫LiabilityUserTransfer()assetRepo呼叫GetAssetWithInit(),如果用戶資產存在則返回,不存在則初始化創建,資產模組在需要使用用戶資產時才創建他,不需先預載用戶資料表,也因為如此,在進入撮合系統前的auth非常重要,必須是認證過的用戶才能進入資產模組。

liability user呼叫SubAssetAvailable()減少資產,用戶呼叫AddAssetAvailable()獲取資產,資產的變動會呼叫transferResult.addUserAsset()添加至transferResult,最後回應給下游服務。

func (u *userAsset) LiabilityUserTransfer(ctx context.Context, toUserID, assetID int, amount decimal.Decimal) (*domain.TransferResult, error) {
	if amount.LessThanOrEqual(decimal.Zero) {
		return nil, errors.New("can not operate less or equal zero amount")
	}

	transferResult := createTransferResult()

	liabilityUserAsset, err := u.assetRepo.GetAssetWithInit(domain.LiabilityUserID, assetID)
	if err != nil {
		return nil, errors.Wrap(err, "get asset with init failed")
	}
	toUserAsset, err := u.assetRepo.GetAssetWithInit(toUserID, assetID)
	if err != nil {
		return nil, errors.Wrap(err, "get asset with init failed")
	}

	u.assetRepo.SubAssetAvailable(liabilityUserAsset, amount)
	u.assetRepo.AddAssetAvailable(toUserAsset, amount)

	transferResult.addUserAsset(liabilityUserAsset)
	transferResult.addUserAsset(toUserAsset)

	return transferResult.TransferResult, nil
}

其他的use case都與LiabilityUserTransfer()類似,只是資產的轉移有差異,最後都會透過transferResult將變動的資產回應給下游服務。

在創建訂單時呼叫Freeze(),先檢查用戶是否有足夠資產userAsset.Available.Cmp(amount) < 0,如果足夠則呼叫SubAssetAvailable()減少資產,並呼叫AddAssetFrozen()增加凍結資產。

func (u *userAsset) Freeze(ctx context.Context, userID, assetID int, amount decimal.Decimal) (*domain.TransferResult, error) {
	// another code...

	if userAsset.Available.Cmp(amount) < 0 {
		return nil, errors.Wrap(domain.LessAmountErr, "less amount err")
	}
	u.assetRepo.SubAssetAvailable(userAsset, amount)
	u.assetRepo.AddAssetFrozen(userAsset, amount)

	transferResult.addUserAsset(userAsset)

	return transferResult.TransferResult, nil
}

在撮合訂單時呼叫TransferFrozenToAvailable(),先檢查用戶(fromUser)是否有足夠的凍結資產後呼叫SubAssetFrozen()減少凍結用戶(fromUser)資產,並呼叫AddAssetAvailable()增加用戶(toUser)資產。

func (u *userAsset) TransferFrozenToAvailable(ctx context.Context, fromUserID, toUserID, assetID int, amount decimal.Decimal) (*domain.TransferResult, error) {
	// another code...

	if fromUserAsset.Frozen.Cmp(amount) < 0 {
		return nil, errors.Wrap(domain.LessAmountErr, "less amount err")
	}
	u.assetRepo.SubAssetFrozen(fromUserAsset, amount)
	u.assetRepo.AddAssetAvailable(toUserAsset, amount)

	transferResult.addUserAsset(fromUserAsset)
	transferResult.addUserAsset(toUserAsset)

	return transferResult.TransferResult, nil
}

在用戶轉帳時呼叫TransferAvailableToAvailable(),檢查用戶(fromUser)是否有足夠資產後呼叫SubAssetAvailable()減少資產並呼叫AddAssetAvailable()增加用戶(toUser)資產。

func (u *userAsset) TransferAvailableToAvailable(ctx context.Context, fromUserID, toUserID, assetID int, amount decimal.Decimal) (*domain.TransferResult, error) {
	// another code...

	if fromUserAsset.Available.Cmp(amount) < 0 {
		return nil, errors.Wrap(domain.LessAmountErr, "less amount err")
	}
	u.assetRepo.SubAssetAvailable(fromUserAsset, amount)
	u.assetRepo.AddAssetAvailable(toUserAsset, amount)

	transferResult.addUserAsset(fromUserAsset)
	transferResult.addUserAsset(toUserAsset)

	return transferResult.TransferResult, nil
}
comments powered by Disqus