From 350f5147b8f23da4060635ad1413571f00c9e3ad Mon Sep 17 00:00:00 2001
From: Jono Wenger <jono@elixxir.io>
Date: Tue, 7 Mar 2023 11:43:10 -0800
Subject: [PATCH] Write bindings for channels file transfer

---
 go.mod                            |   2 +-
 go.sum                            |   4 +-
 main.go                           |   4 +
 wasm/channelsFileTransfer.go      | 558 ++++++++++++++++++++++++++++++
 wasm/channelsFileTransfer_test.go |  98 ++++++
 wasm/docs.go                      |   3 +
 6 files changed, 666 insertions(+), 3 deletions(-)
 create mode 100644 wasm/channelsFileTransfer.go
 create mode 100644 wasm/channelsFileTransfer_test.go

diff --git a/go.mod b/go.mod
index 40d74793..c6142470 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/jwalterweatherman v1.1.0
-	gitlab.com/elixxir/client/v4 v4.3.12-0.20230306215020-e4b1c0ae13fd
+	gitlab.com/elixxir/client/v4 v4.3.12-0.20230307194033-15078a6a49d0
 	gitlab.com/elixxir/crypto v0.0.7-0.20230216203124-0c064fe2e78f
 	gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c
 	gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd
diff --git a/go.sum b/go.sum
index 6dd5edd1..ef30d0f9 100644
--- a/go.sum
+++ b/go.sum
@@ -401,8 +401,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40=
 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k=
-gitlab.com/elixxir/client/v4 v4.3.12-0.20230306215020-e4b1c0ae13fd h1:yZxRtcy051JlOgT37UhO80B+cw8/OnO6gZtM6uafFfY=
-gitlab.com/elixxir/client/v4 v4.3.12-0.20230306215020-e4b1c0ae13fd/go.mod h1:Hjx99EdI86q67mHzZVR2Dw37fuTCzDaChM/NVX3CcPU=
+gitlab.com/elixxir/client/v4 v4.3.12-0.20230307194033-15078a6a49d0 h1:3YawABe4MiVLxP+VA2IBVkp8ud26DNYmkeuaK2pjwnI=
+gitlab.com/elixxir/client/v4 v4.3.12-0.20230307194033-15078a6a49d0/go.mod h1:4FWaMf01ZLBExwU1TWWTy2WhyzXzicTqZSDNSqKp3pA=
 gitlab.com/elixxir/comms v0.0.4-0.20230214180204-3aba2e6795af h1:Eye4+gZEUbOfz4j51WplYD9d7Gnr1s3wKYkEnCfhPaw=
 gitlab.com/elixxir/comms v0.0.4-0.20230214180204-3aba2e6795af/go.mod h1:ud3s2aHx5zu7lJhBpUMUXxjLwl8PH8z8cl64Om9U7q8=
 gitlab.com/elixxir/crypto v0.0.7-0.20230216203124-0c064fe2e78f h1:GPUuaSKaDULcw/obfTHYEV2In2CAchnYEvknYWwDw+4=
diff --git a/main.go b/main.go
index f170f646..4b82dc9d 100644
--- a/main.go
+++ b/main.go
@@ -94,6 +94,10 @@ func main() {
 	js.Global().Set("NewChannelsDatabaseCipher",
 		js.FuncOf(wasm.NewChannelsDatabaseCipher))
 
+	// wasm/dm.go
+	js.Global().Set("InitChannelsFileTransfer",
+		js.FuncOf(wasm.InitChannelsFileTransfer))
+
 	// wasm/dm.go
 	js.Global().Set("NewDMClient", js.FuncOf(wasm.NewDMClient))
 	js.Global().Set("NewDMClientWithIndexedDb",
diff --git a/wasm/channelsFileTransfer.go b/wasm/channelsFileTransfer.go
new file mode 100644
index 00000000..95fa8fc3
--- /dev/null
+++ b/wasm/channelsFileTransfer.go
@@ -0,0 +1,558 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package wasm
+
+import (
+	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"syscall/js"
+)
+
+// ChannelsFileTransfer wraps the [bindings.ChannelsFileTransfer] object so its
+// methods can be wrapped to be Javascript compatible.
+type ChannelsFileTransfer struct {
+	api *bindings.ChannelsFileTransfer
+}
+
+// newChannelsFileTransferJS creates a new Javascript compatible object
+// (map[string]any) that matches the [ChannelsFileTransfer] structure.
+func newChannelsFileTransferJS(api *bindings.ChannelsFileTransfer) map[string]any {
+	cft := ChannelsFileTransfer{api}
+	channelsFileTransferMap := map[string]any{
+		"MaxFileNameLen": js.FuncOf(cft.MaxFileNameLen),
+		"MaxFileTypeLen": js.FuncOf(cft.MaxFileTypeLen),
+		"MaxFileSize":    js.FuncOf(cft.MaxFileSize),
+		"MaxPreviewSize": js.FuncOf(cft.MaxPreviewSize),
+
+		// Uploading/Sending
+		"Upload":                       js.FuncOf(cft.Upload),
+		"Send":                         js.FuncOf(cft.Send),
+		"RegisterSentProgressCallback": js.FuncOf(cft.RegisterSentProgressCallback),
+		"RetryUpload":                  js.FuncOf(cft.RetryUpload),
+		"CloseSend":                    js.FuncOf(cft.CloseSend),
+
+		// Downloading
+		"Download":                         js.FuncOf(cft.Download),
+		"RegisterReceivedProgressCallback": js.FuncOf(cft.RegisterReceivedProgressCallback),
+	}
+
+	return channelsFileTransferMap
+}
+
+// InitChannelsFileTransfer creates a file transfer manager for channels.
+//
+// Parameters:
+//   - args[0] - ID of [E2e] object in tracker (int).
+//   - args[1] - JSON of [channelsFileTransfer.Params] (Uint8Array).
+//
+// Returns:
+//   - New [ChannelsFileTransfer] object.
+//
+// Returns a promise:
+//   - Resolves to a Javascript representation of the [ChannelsFileTransfer]
+//     object.
+//   - Rejected with an error if creating the file transfer object fails.
+func InitChannelsFileTransfer(_ js.Value, args []js.Value) any {
+	e2eID := args[0].Int()
+	paramsJson := utils.CopyBytesToGo(args[1])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		cft, err := bindings.InitChannelsFileTransfer(e2eID, paramsJson)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(newChannelsFileTransferJS(cft))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// MaxFileNameLen returns the max number of bytes allowed for a file name.
+//
+// Returns:
+//   - Max number of bytes (int).
+func (cft *ChannelsFileTransfer) MaxFileNameLen(js.Value, []js.Value) any {
+	return cft.api.MaxFileNameLen()
+}
+
+// MaxFileTypeLen returns the max number of bytes allowed for a file type.
+//
+// Returns:
+//   - Max number of bytes (int).
+func (cft *ChannelsFileTransfer) MaxFileTypeLen(js.Value, []js.Value) any {
+	return cft.api.MaxFileNameLen()
+}
+
+// MaxFileSize returns the max number of bytes allowed for a file.
+//
+// Returns:
+//   - Max number of bytes (int).
+func (cft *ChannelsFileTransfer) MaxFileSize(js.Value, []js.Value) any {
+	return cft.api.MaxFileSize()
+}
+
+// MaxPreviewSize returns the max number of bytes allowed for a file preview.
+//
+// Returns:
+//   - Max number of bytes (int).
+func (cft *ChannelsFileTransfer) MaxPreviewSize(js.Value, []js.Value) any {
+	return cft.api.MaxFileSize()
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Uploading/Sending                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// Upload starts uploading the file to a new ID that can be sent to the
+// specified channel when complete. To get progress information about the
+// upload, a [bindings.FtSentProgressCallback] must be registered. All errors
+// returned on the callback are fatal and the user must take action to either
+// [ChannelsFileTransfer.RetryUpload] or [ChannelsFileTransfer.CloseSend].
+//
+// The file is added to the event model at the returned file ID with the status
+// [channelsFileTransfer.Uploading]. Once the upload is complete, the file link
+// is added to the event model with the status [channelsFileTransfer.Complete].
+//
+// The [bindings.FtSentProgressCallback] only indicates the progress of the file
+// upload, not the status of the file in the event model. You must rely on
+// updates from the event model to know when it can be retrieved.
+//
+// Parameters:
+//   - args[0] - File contents. Max size defined by
+//     [ChannelsFileTransfer.MaxFileSize] (Uint8Array).
+//   - args[1] - The number of sending retries allowed on send failure (e.g. a
+//     retry of 2.0 with 6 parts means 12 total possible sends) (float).
+//   - args[2] - The progress callback, which is a callback that reports the
+//     progress of the file upload. The callback is called once on
+//     initialization, on every progress update (or less if restricted by the
+//     period), or on fatal error. It must be a Javascript object that
+//     implements the [bindings.FtSentProgressCallback] interface.
+//   - args[3] - Progress callback period. A progress callback will be limited
+//     from triggering only once per period, in milliseconds (int).
+//
+// Returns a promise:
+//   - Resolves to the marshalled bytes of [fileTransfer.ID] that uniquely
+//     identifies the file (Uint8Array).
+//   - Rejected with an error if initiating the upload fails.
+func (cft *ChannelsFileTransfer) Upload(_ js.Value, args []js.Value) any {
+	var (
+		fileData   = utils.CopyBytesToGo(args[0])
+		retry      = float32(args[1].Float())
+		progressCB = &ftSentCallback{utils.WrapCB(args[2], "Callback")}
+		period     = args[3].Int()
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		fileID, err := cft.api.Upload(fileData, retry, progressCB, period)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(fileID))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// Send sends the specified file info to the channel. Once a file is uploaded
+// via [ChannelsFileTransfer.Upload], its file info (found in the event model)
+// can be sent to any channel.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel's [id.ID] to send the file to
+//     (Uint8Array).
+//   - args[1] - JSON of [channelsFileTransfer.FileLink] stored in the event
+//     model (Uint8Array).
+//   - args[2] - Human-readable file name. Max length defined by
+//     [ChannelsFileTransfer.MaxFileNameLen] (string).
+//   - args[3] - Shorthand that identifies the type of file. Max length defined
+//     by [ChannelsFileTransfer.MaxFileTypeLen] (string).
+//   - args[4] - A preview of the file data (e.g. a thumbnail). Max size defined
+//     by [ChannelsFileTransfer.MaxPreviewSize] (Uint8Array).
+//   - args[5] - The duration, in milliseconds, that the file is available in
+//     the channel (int). For the maximum amount of time, use [ValidForever].
+//   - args[6] - JSON of [xxdk.CMIXParams] (Uint8Array). If left empty,
+//     [GetDefaultCMixParams] will be used internally.
+//
+// Returns a promise:
+//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//   - Rejected with an error if sending fails.
+func (cft *ChannelsFileTransfer) Send(_ js.Value, args []js.Value) any {
+	var (
+		channelIdBytes = utils.CopyBytesToGo(args[0])
+		fileLinkJSON   = utils.CopyBytesToGo(args[1])
+		fileName       = args[2].String()
+		fileType       = args[3].String()
+		preview        = utils.CopyBytesToGo(args[4])
+		validUntilMS   = args[5].Int()
+		cmixParamsJSON = utils.CopyBytesToGo(args[6])
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON, fileName,
+			fileType, preview, validUntilMS, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(fileID))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// RegisterSentProgressCallback allows for the registration of a callback to
+// track the progress of an individual file upload. A
+// [bindings.FtSentProgressCallback] is auto registered on
+// [ChannelsFileTransfer.Send]; this function should be called when resuming
+// clients or registering extra callbacks.
+//
+// The callback will be called immediately when added to report the current
+// progress of the transfer. It will then call every time a file part arrives,
+// the transfer completes, or a fatal error occurs. It is called at most once
+// every period regardless of the number of progress updates.
+//
+// In the event that the client is closed and resumed, this function must be
+// used to re-register any callbacks previously registered with this function or
+// [ChannelsFileTransfer.Send].
+//
+// The [bindings.FtSentProgressCallback] only indicates the progress of the file
+// upload, not the status of the file in the event model. You must rely on
+// updates from the event model to know when it can be retrieved.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array).
+//   - args[1] - The progress callback, which is a callback that reports the
+//     progress of the file upload. The callback is called once on
+//     initialization, on every progress update (or less if restricted by the
+//     period), or on fatal error. It must be a Javascript object that
+//     implements the [bindings.FtSentProgressCallback] interface.
+//   - args[2] - Progress callback period. A progress callback will be limited
+//     from triggering only once per period, in milliseconds (int).
+//
+// Returns a promise:
+//   - Resolves on success (void).
+//   - Rejected with an error if registering the callback fails.
+func (cft *ChannelsFileTransfer) RegisterSentProgressCallback(
+	_ js.Value, args []js.Value) any {
+	var (
+		fileIDBytes = utils.CopyBytesToGo(args[0])
+		progressCB  = &ftSentCallback{utils.WrapCB(args[1], "Callback")}
+		periodMS    = args[2].Int()
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := cft.api.RegisterSentProgressCallback(
+			fileIDBytes, progressCB, periodMS)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// RetryUpload retries uploading a failed file upload. Returns an error if the
+// transfer has not failed.
+//
+// This function should be called once a transfer errors out (as reported by the
+// progress callback).
+//
+// A new progress callback must be registered on retry. Any previously
+// registered callbacks are defunct when the upload fails.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array).
+//   - args[1] - The progress callback, which is a callback that reports the
+//     progress of the file upload. The callback is called once on
+//     initialization, on every progress update (or less if restricted by the
+//     period), or on fatal error. It must be a Javascript object that
+//     implements the [bindings.FtSentProgressCallback] interface.
+//   - args[2] - Progress callback period. A progress callback will be limited
+//     from triggering only once per period, in milliseconds (int).
+//
+// Returns a promise:
+//   - Resolves on success (void).
+//   - Rejected with an error if registering retrying the upload fails.
+func (cft *ChannelsFileTransfer) RetryUpload(_ js.Value, args []js.Value) any {
+	var (
+		fileIDBytes = utils.CopyBytesToGo(args[0])
+		progressCB  = &ftSentCallback{utils.WrapCB(args[1], "Callback")}
+		periodMS    = args[2].Int()
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := cft.api.RetryUpload(fileIDBytes, progressCB, periodMS)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// CloseSend deletes a file from the internal storage once a transfer has
+// completed or reached the retry limit. If neither of those condition are met,
+// an error is returned.
+//
+// This function should be called once a transfer completes or errors out (as
+// reported by the progress callback).
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array).
+//
+// Returns a promise:
+//   - Resolves on success (void).
+//   - Rejected with an error if the file has not failed or completed or if
+//     closing failed.
+func (cft *ChannelsFileTransfer) CloseSend(_ js.Value, args []js.Value) any {
+	fileIDBytes := utils.CopyBytesToGo(args[0])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := cft.api.CloseSend(fileIDBytes)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Download                                                                   //
+////////////////////////////////////////////////////////////////////////////////
+
+// Download begins the download of the file described in the marshalled
+// [channelsFileTransfer.FileInfo]. The progress of the download is reported on
+// the [bindings.FtReceivedProgressCallback].
+//
+// Once the download completes, the file will be stored in the event model with
+// the given file ID and with the status [channels.ReceptionProcessingComplete].
+//
+// The [bindings.FtReceivedProgressCallback] only indicates the progress of the
+// file download, not the status of the file in the event model. You must rely
+// on updates from the event model to know when it can be retrieved.
+//
+// Parameters:
+//   - args[0] - The JSON of [channelsFileTransfer.FileInfo] received on a
+//     channel (Uint8Array).
+//   - args[1] - The progress callback, which is a callback that reports the
+//     progress of the file download. The callback is called once on
+//     initialization, on every progress update (or less if restricted by the
+//     period), or on fatal error. It must be a Javascript object that
+//     implements the [bindings.FtReceivedProgressCallback] interface.
+//   - args[2] - Progress callback period. A progress callback will be limited
+//     from triggering only once per period, in milliseconds (int).
+//
+// Returns:
+//   - Marshalled bytes of [fileTransfer.ID] that uniquely identifies the file.
+//
+// Returns a promise:
+//   - Resolves to the marshalled bytes of [fileTransfer.ID] that uniquely
+//     identifies the file. (Uint8Array).
+//   - Rejected with an error if downloading fails.
+func (cft *ChannelsFileTransfer) Download(_ js.Value, args []js.Value) any {
+	var (
+		fileInfoJSON = utils.CopyBytesToGo(args[0])
+		progressCB   = &ftReceivedCallback{utils.WrapCB(args[1], "Callback")}
+		periodMS     = args[2].Int()
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		fileID, err := cft.api.Download(fileInfoJSON, progressCB, periodMS)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(fileID))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// RegisterReceivedProgressCallback allows for the registration of a callback to
+// track the progress of an individual received file transfer.
+//
+// The callback will be called immediately when added to report the current
+// progress of the transfer. It will then call every time a file part is
+// received, the transfer completes, or a fatal error occurs. It is called at
+// most once every period regardless of the number of progress updates.
+//
+// In the event that the client is closed and resumed, this function must be
+// used to re-register any callbacks previously registered.
+//
+// Once the download completes, the file will be stored in the event model with
+// the given file ID and with the status [channels.ReceptionProcessingComplete].
+//
+// The [bindings.FtReceivedProgressCallback] only indicates the progress of the
+// file download, not the status of the file in the event model. You must rely
+// on updates from the event model to know when it can be retrieved.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array).
+//   - args[1] - The progress callback, which is a callback that reports the
+//     progress of the file download. The callback is called once on
+//     initialization, on every progress update (or less if restricted by the
+//     period), or on fatal error. It must be a Javascript object that
+//     implements the [bindings.FtReceivedProgressCallback] interface.
+//   - args[2] - Progress callback period. A progress callback will be limited
+//     from triggering only once per period, in milliseconds (int).
+//
+// Returns a promise:
+//   - Resolves on success (void).
+//   - Rejected with an error if registering the callback fails.
+func (cft *ChannelsFileTransfer) RegisterReceivedProgressCallback(
+	_ js.Value, args []js.Value) any {
+	var (
+		fileIDBytes = utils.CopyBytesToGo(args[0])
+		progressCB  = &ftReceivedCallback{utils.WrapCB(args[1], "Callback")}
+		periodMS    = args[2].Int()
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := cft.api.RegisterReceivedProgressCallback(
+			fileIDBytes, progressCB, periodMS)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Callbacks                                                                  //
+////////////////////////////////////////////////////////////////////////////////
+
+// ftSentCallback wraps Javascript callbacks to adhere to the
+// [bindings.FtSentProgressCallback] interface.
+type ftSentCallback struct {
+	callback func(args ...any) js.Value
+}
+
+// Callback is called when the status of the sent file changes.
+//
+// Parameters:
+//   - payload - Returns the contents of the message. JSON of
+//     [bindings.Progress] (Uint8Array).
+//   - t - Returns a tracker that allows the lookup of the status of any file
+//     part. It is a Javascript object that matches the functions on
+//     [FilePartTracker].
+//   - err - Returns an error on failure (Error).
+
+// Callback is called when the progress on a sent file changes or an error
+// occurs in the transfer.
+//
+// The [ChFilePartTracker] can be used to look up the status of individual file
+// parts. Note, when completed == true, the [ChFilePartTracker] may be nil.
+//
+// Any error returned is fatal and the file must either be retried with
+// [ChannelsFileTransfer.RetryUpload] or canceled with
+// [ChannelsFileTransfer.CloseSend].
+//
+// This callback only indicates the status of the file transfer, not the status
+// of the file in the event model. Do NOT use this callback as an indicator of
+// when the file is available in the event model.
+//
+// Parameters:
+//   - payload - JSON of [bindings.FtSentProgress], which describes the progress
+//     of the current sent transfer.
+//   - fpt - File part tracker that allows the lookup of the status of
+//     individual file parts.
+//   - err - Fatal errors during sending.
+func (fsc *ftSentCallback) Callback(
+	payload []byte, t *bindings.ChFilePartTracker, err error) {
+	fsc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t),
+		utils.JsTrace(err))
+}
+
+// ftReceivedCallback wraps Javascript callbacks to adhere to the
+// [bindings.FtReceivedProgressCallback] interface.
+type ftReceivedCallback struct {
+	callback func(args ...any) js.Value
+}
+
+// Callback is called when
+// the progress on a received file changes or an error occurs in the transfer.
+//
+// The [ChFilePartTracker] can be used to look up the status of individual file
+// parts. Note, when completed == true, the [ChFilePartTracker] may be nil.
+//
+// This callback only indicates the status of the file transfer, not the status
+// of the file in the event model. Do NOT use this callback as an indicator of
+// when the file is available in the event model.
+//
+// Parameters:
+//   - payload - JSON of [bindings.FtReceivedProgress], which describes the
+//     progress of the current received transfer.
+//   - fpt - File part tracker that allows the lookup of the status of
+//     individual file parts.
+//   - err - Fatal errors during receiving.
+func (frc *ftReceivedCallback) Callback(
+	payload []byte, t *bindings.ChFilePartTracker, err error) {
+	frc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t),
+		utils.JsTrace(err))
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// File Part Tracker                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// ChFilePartTracker wraps the [bindings.ChFilePartTracker] object so its
+// methods can be wrapped to be Javascript compatible.
+type ChFilePartTracker struct {
+	api *bindings.ChFilePartTracker
+}
+
+// newChFilePartTrackerJS creates a new Javascript compatible object
+// (map[string]any) that matches the [FilePartTracker] structure.
+func newChFilePartTrackerJS(api *bindings.ChFilePartTracker) map[string]any {
+	fpt := ChFilePartTracker{api}
+	ftMap := map[string]any{
+		"GetPartStatus": js.FuncOf(fpt.GetPartStatus),
+		"GetNumParts":   js.FuncOf(fpt.GetNumParts),
+	}
+
+	return ftMap
+}
+
+// GetPartStatus returns the status of the file part with the given part number.
+//
+// The possible values for the status are:
+//   - 0 < Part does not exist
+//   - 0 = unsent
+//   - 1 = arrived (sender has sent a part, and it has arrived)
+//   - 2 = received (receiver has received a part)
+//
+// Parameters:
+//   - args[0] - Index of part (int).
+//
+// Returns:
+//   - Part status (int).
+func (fpt *ChFilePartTracker) GetPartStatus(_ js.Value, args []js.Value) any {
+	return fpt.api.GetPartStatus(args[0].Int())
+}
+
+// GetNumParts returns the total number of file parts in the transfer.
+//
+// Returns:
+//   - Number of parts (int).
+func (fpt *ChFilePartTracker) GetNumParts(js.Value, []js.Value) any {
+	return fpt.api.GetNumParts()
+}
diff --git a/wasm/channelsFileTransfer_test.go b/wasm/channelsFileTransfer_test.go
new file mode 100644
index 00000000..d697de63
--- /dev/null
+++ b/wasm/channelsFileTransfer_test.go
@@ -0,0 +1,98 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package wasm
+
+import (
+	"gitlab.com/elixxir/client/v4/bindings"
+	"reflect"
+	"testing"
+)
+
+// Tests that the map representing ChannelsFileTransfer returned by
+// newChannelsFileTransferJS contains all of the methods on ChannelsFileTransfer.
+func Test_newChannelsFileTransferJS(t *testing.T) {
+	cftType := reflect.TypeOf(&ChannelsFileTransfer{})
+
+	ft := newChannelsFileTransferJS(&bindings.ChannelsFileTransfer{})
+	if len(ft) != cftType.NumMethod() {
+		t.Errorf("ChannelsFileTransfer JS object does not have all methods."+
+			"\nexpected: %d\nreceived: %d", cftType.NumMethod(), len(ft))
+	}
+
+	for i := 0; i < cftType.NumMethod(); i++ {
+		method := cftType.Method(i)
+
+		if _, exists := ft[method.Name]; !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
+// Tests that ChannelsFileTransfer has all the methods that
+// [bindings.ChannelsFileTransfer] has.
+func Test_ChannelsFileTransferMethods(t *testing.T) {
+	cftType := reflect.TypeOf(&ChannelsFileTransfer{})
+	binCftType := reflect.TypeOf(&bindings.ChannelsFileTransfer{})
+
+	if binCftType.NumMethod() != cftType.NumMethod() {
+		t.Errorf("WASM ChannelsFileTransfer object does not have all methods "+
+			"from bindings.\nexpected: %d\nreceived: %d",
+			binCftType.NumMethod(), cftType.NumMethod())
+	}
+
+	for i := 0; i < binCftType.NumMethod(); i++ {
+		method := binCftType.Method(i)
+
+		if _, exists := cftType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
+// Tests that the map representing ChFilePartTracker returned by
+// newChFilePartTrackerJS contains all of the methods on ChFilePartTracker.
+func Test_newChFilePartTrackerJS(t *testing.T) {
+	fptType := reflect.TypeOf(&FilePartTracker{})
+
+	fpt := newChFilePartTrackerJS(&bindings.ChFilePartTracker{})
+	if len(fpt) != fptType.NumMethod() {
+		t.Errorf("ChFilePartTracker JS object does not have all methods."+
+			"\nexpected: %d\nreceived: %d", fptType.NumMethod(), len(fpt))
+	}
+
+	for i := 0; i < fptType.NumMethod(); i++ {
+		method := fptType.Method(i)
+
+		if _, exists := fpt[method.Name]; !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
+// Tests that ChFilePartTracker has all the methods that
+// [bindings.ChFilePartTracker] has.
+func Test_ChFilePartTrackerMethods(t *testing.T) {
+	fptType := reflect.TypeOf(&ChFilePartTracker{})
+	binFptType := reflect.TypeOf(&bindings.ChFilePartTracker{})
+
+	if binFptType.NumMethod() != fptType.NumMethod() {
+		t.Errorf("WASM ChFilePartTracker object does not have all methods from "+
+			"bindings.\nexpected: %d\nreceived: %d",
+			binFptType.NumMethod(), fptType.NumMethod())
+	}
+
+	for i := 0; i < binFptType.NumMethod(); i++ {
+		method := binFptType.Method(i)
+
+		if _, exists := fptType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/docs.go b/wasm/docs.go
index b2d6f489..5b8e7a6d 100644
--- a/wasm/docs.go
+++ b/wasm/docs.go
@@ -15,6 +15,7 @@ import (
 	"gitlab.com/elixxir/client/v4/auth"
 	"gitlab.com/elixxir/client/v4/catalog"
 	"gitlab.com/elixxir/client/v4/channels"
+	"gitlab.com/elixxir/client/v4/channelsFileTransfer"
 	"gitlab.com/elixxir/client/v4/cmix"
 	"gitlab.com/elixxir/client/v4/cmix/message"
 	"gitlab.com/elixxir/client/v4/connect"
@@ -69,4 +70,6 @@ var (
 	_ = broadcast.Channel{}
 	_ = netTime.Now
 	_ = ed25519.PublicKey{}
+	_ = channelsFileTransfer.Params{}
+	_ = fileTransfer.ID{}
 )
-- 
GitLab