diff --git a/Makefile b/Makefile
index 193a72db0633569291863a33b0bb8f0afab7bb0d..f8c143a6a24f68f6b70d2bc64cc6028b56f4c9af 100644
--- a/Makefile
+++ b/Makefile
@@ -11,11 +11,11 @@ build:
 	GOOS=js GOARCH=wasm go build ./...
 
 update_release:
-	GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release
-	GOFLAGS="" go get gitlab.com/elixxir/crypto@release
+	GOFLAGS="" go get gitlab.com/xx_network/primitives@release
 	GOFLAGS="" go get gitlab.com/elixxir/primitives@release
 	GOFLAGS="" go get gitlab.com/xx_network/crypto@release
-	GOFLAGS="" go get gitlab.com/xx_network/primitives@release
+	GOFLAGS="" go get gitlab.com/elixxir/crypto@release
+	GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release
 
 update_master:
 	GOFLAGS="" go get -d gitlab.com/elixxir/client@master
diff --git a/go.mod b/go.mod
index c259f8c90bf14352c7d6428cb4210911bef52f47..3ab70b64473519782a56ca71e5a1a21e24c7ec29 100644
--- a/go.mod
+++ b/go.mod
@@ -7,8 +7,8 @@ require (
 	github.com/hack-pad/go-indexeddb v0.2.0
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/jwalterweatherman v1.1.0
-	gitlab.com/elixxir/client/v4 v4.6.2-0.20230413170823-18dc8e973dfb
-	gitlab.com/elixxir/crypto v0.0.7-0.20230322181929-8cb5fa100824
+	gitlab.com/elixxir/client/v4 v4.6.2-0.20230413171204-002612660098
+	gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32
 	gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c
 	gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd
 	gitlab.com/xx_network/primitives v0.0.4-0.20230310205521-c440e68e34c4
diff --git a/go.sum b/go.sum
index a82bd19e13e7d402351692a5751ee4c0dc7869d2..3bed21b3c592d81e5ca980ecc6f26831f302764c 100644
--- a/go.sum
+++ b/go.sum
@@ -391,12 +391,12 @@ 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-20230315224936-a4459418f300 h1:oF3Pkf5NBb48KB89Q4sQXKQCIsWp1IVsqKWHWFsfBRc=
 gitlab.com/elixxir/bloomfilter v0.0.0-20230315224936-a4459418f300/go.mod h1:1X8gRIAPDisS3W6Vtr/ymiUmZMJUIwDV1o5DEOo/pzw=
-gitlab.com/elixxir/client/v4 v4.6.2-0.20230413170823-18dc8e973dfb h1:xLxLy8g8NwUgYR+P5hUbUb35SRZeuv/HZErmlbIkBgs=
-gitlab.com/elixxir/client/v4 v4.6.2-0.20230413170823-18dc8e973dfb/go.mod h1:WEYVoIXHi2YMR0JafS5pHNWLOtnkLBBt8KdhKF/ZHcY=
+gitlab.com/elixxir/client/v4 v4.6.2-0.20230413171204-002612660098 h1:bdwXgEa0i9KpLiKQdhv6MEWAYLt3MsbNuIzFanVpWLY=
+gitlab.com/elixxir/client/v4 v4.6.2-0.20230413171204-002612660098/go.mod h1:G+lN+LvQPGcm5BQnrhnqT1xiRIAzH3OffAM+5oI9SUg=
 gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b h1:8AVK93UEs/aufoqtFgyMVt9gf0oJ8F4pA60ZvEVvG+s=
 gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q=
-gitlab.com/elixxir/crypto v0.0.7-0.20230322181929-8cb5fa100824 h1:6gmaBG4glJKA41SV2tNBbT6mFwTEXR9Jn9JaU6JSSKM=
-gitlab.com/elixxir/crypto v0.0.7-0.20230322181929-8cb5fa100824/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc=
+gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32 h1:Had0F7rMPgJJ2BUZoFNgeJq33md9RpV15nvd08Uxdzc=
+gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc=
 gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is=
 gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU=
 gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c h1:muG8ff95woeVVwQoJHCEclxBFB22lc7EixPylEkYDRU=
diff --git a/indexedDb/impl/channels/callbacks.go b/indexedDb/impl/channels/callbacks.go
index b3add64fcf95da83000434c784e96199a5fc3c0e..df560535a779f2e6c355c6e49a66d4b45c2047e2 100644
--- a/indexedDb/impl/channels/callbacks.go
+++ b/indexedDb/impl/channels/callbacks.go
@@ -258,8 +258,12 @@ func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) {
 		status = &msg.Status
 	}
 
-	m.model.UpdateFromUUID(
+	err = m.model.UpdateFromUUID(
 		msg.UUID, messageID, timestamp, round, pinned, hidden, status)
+	if err != nil {
+		return []byte(err.Error()), nil
+	}
+
 	return nil, nil
 }
 
@@ -292,15 +296,21 @@ func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) {
 		status = &msg.Status
 	}
 
-	uuid := m.model.UpdateFromMessageID(
+	var ue wChannels.UuidError
+	uuid, err := m.model.UpdateFromMessageID(
 		msg.MessageID, timestamp, round, pinned, hidden, status)
+	if err != nil {
+		ue.Error = []byte(err.Error())
+	} else {
+		ue.UUID = uuid
+	}
 
-	uuidData, err := json.Marshal(uuid)
+	data, err = json.Marshal(ue)
 	if err != nil {
-		return nil, errors.Errorf("failed to JSON marshal UUID: %+v", err)
+		return nil, errors.Errorf("failed to JSON marshal %T: %+v", ue, err)
 	}
 
-	return uuidData, nil
+	return data, nil
 }
 
 // getMessageCB is the callback for wasmModel.GetMessage. Returns JSON
diff --git a/indexedDb/impl/channels/implementation.go b/indexedDb/impl/channels/implementation.go
index 627dd313a36974be2ff40ead5b00ee9f0635bfd3..54ec455870bb77645ec76f4e2cea7043bd5997e1 100644
--- a/indexedDb/impl/channels/implementation.go
+++ b/indexedDb/impl/channels/implementation.go
@@ -13,6 +13,7 @@ import (
 	"crypto/ed25519"
 	"encoding/json"
 	"strconv"
+	"strings"
 	"syscall/js"
 	"time"
 
@@ -21,9 +22,11 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 
 	"gitlab.com/elixxir/client/v4/channels"
+	cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fileTransfer"
 	"gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
 	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
@@ -44,6 +47,131 @@ type wasmModel struct {
 	mutedUserCB       wChannels.MutedUserCallback
 }
 
+// ReceiveFile is called when a file upload or download beings.
+//
+// fileLink and fileData are nillable and may be updated based
+// upon the UUID or file ID later.
+//
+// fileID is always unique to the fileData. fileLink is the JSON of
+// channelsFileTransfer.FileLink.
+//
+// Returns any fatal errors.
+func (w *wasmModel) ReceiveFile(fileID fileTransfer.ID, fileLink,
+	fileData []byte, timestamp time.Time, status cft.Status) error {
+
+	newFile := &File{
+		Id:        fileID.Marshal(),
+		Data:      fileData,
+		Link:      fileLink,
+		Timestamp: timestamp,
+		Status:    uint8(status),
+	}
+	return w.upsertFile(newFile)
+}
+
+// UpdateFile is called when a file upload or download completes or changes.
+//
+// fileLink, fileData, timestamp, and status are all nillable and may be
+// updated based upon the file ID at a later date. If a nil value is passed,
+// then make no update.
+//
+// Returns an error if the file cannot be updated. It must return
+// channels.NoMessageErr if the file does not exist.
+func (w *wasmModel) UpdateFile(fileID fileTransfer.ID, fileLink,
+	fileData []byte, timestamp *time.Time, status *cft.Status) error {
+	parentErr := "[Channels indexedDB] failed to UpdateFile"
+
+	// Get the File as it currently exists in storage
+	fileObj, err := impl.Get(w.db, fileStoreName, impl.EncodeBytes(fileID.Marshal()))
+	if err != nil {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
+			return errors.WithMessage(channels.NoMessageErr, parentErr)
+		}
+		return errors.WithMessage(err, parentErr)
+	}
+	currentFile, err := valueToFile(fileObj)
+	if err != nil {
+		return errors.WithMessage(err, parentErr)
+	}
+
+	// Update the fields if specified
+	if status != nil {
+		currentFile.Status = uint8(*status)
+	}
+	if timestamp != nil {
+		currentFile.Timestamp = *timestamp
+	}
+	if fileData != nil {
+		currentFile.Data = fileData
+	}
+	if fileLink != nil {
+		currentFile.Link = fileLink
+	}
+
+	return w.upsertFile(currentFile)
+}
+
+// upsertFile is a helper function that will update an existing File
+// if File.Id is specified. Otherwise, it will perform an insert.
+func (w *wasmModel) upsertFile(newFile *File) error {
+	newFileJson, err := json.Marshal(&newFile)
+	if err != nil {
+		return err
+	}
+	fileObj, err := utils.JsonToJS(newFileJson)
+	if err != nil {
+		return err
+	}
+
+	_, err = impl.Put(w.db, fileStoreName, fileObj)
+	return err
+}
+
+// GetFile returns the ModelFile containing the file data and download link
+// for the given file ID.
+//
+// Returns an error if the file cannot be retrieved. It must return
+// channels.NoMessageErr if the file does not exist.
+func (w *wasmModel) GetFile(fileID fileTransfer.ID) (
+	cft.ModelFile, error) {
+	fileObj, err := impl.Get(w.db, fileStoreName,
+		impl.EncodeBytes(fileID.Marshal()))
+	if err != nil {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
+			return cft.ModelFile{}, channels.NoMessageErr
+		}
+		return cft.ModelFile{}, err
+	}
+
+	resultFile, err := valueToFile(fileObj)
+	if err != nil {
+		return cft.ModelFile{}, err
+	}
+
+	result := cft.ModelFile{
+		ID:        fileTransfer.NewID(resultFile.Data),
+		Link:      resultFile.Link,
+		Data:      resultFile.Data,
+		Timestamp: resultFile.Timestamp,
+		Status:    cft.Status(resultFile.Status),
+	}
+	return result, nil
+}
+
+// DeleteFile deletes the file with the given file ID.
+//
+// Returns fatal errors. It must return channels.NoMessageErr if the file
+// does not exist.
+func (w *wasmModel) DeleteFile(fileID fileTransfer.ID) error {
+	err := impl.Delete(w.db, fileStoreName, impl.EncodeBytes(fileID.Marshal()))
+	if err != nil {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
+			return channels.NoMessageErr
+		}
+	}
+	return err
+}
+
 // JoinChannel is called whenever a channel is joined locally.
 func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 	parentErr := errors.New("failed to JoinChannel")
@@ -69,7 +197,7 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 		return
 	}
 
-	_, err = impl.Put(w.db, channelsStoreName, channelObj)
+	_, err = impl.Put(w.db, channelStoreName, channelObj)
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
 			"Unable to put Channel: %+v", err))
@@ -81,7 +209,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) {
 	parentErr := errors.New("failed to LeaveChannel")
 
 	// Delete the channel from storage
-	err := impl.Delete(w.db, channelsStoreName, js.ValueOf(channelID.String()))
+	err := impl.Delete(w.db, channelStoreName, js.ValueOf(channelID.String()))
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
 			"Unable to delete Channel: %+v", err))
@@ -253,10 +381,13 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
 // messageID, timestamp, round, pinned, and hidden are all nillable and may be
 // updated based upon the UUID at a later date. If a nil value is passed, then
 // make no update.
+//
+// Returns an error if the message cannot be updated. It must return
+// channels.NoMessageErr if the message does not exist.
 func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
 	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
-	status *channels.SentStatus) {
-	parentErr := errors.New("failed to UpdateFromUUID")
+	status *channels.SentStatus) error {
+	parentErr := "failed to UpdateFromUUID"
 
 	// Convert messageID to the key generated by json.Marshal
 	key := js.ValueOf(uuid)
@@ -264,24 +395,24 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
 	// Use the key to get the existing Message
 	msgObj, err := impl.Get(w.db, messageStoreName, key)
 	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to get message: %+v", err))
-		return
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
+			return errors.WithMessage(channels.NoMessageErr, parentErr)
+		}
+		return errors.WithMessage(err, parentErr)
 	}
 
 	currentMsg, err := valueToMessage(msgObj)
 	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Failed to marshal Message: %+v", err))
-		return
+		return errors.WithMessagef(err,
+			"%s Failed to marshal Message", parentErr)
 	}
 
 	_, err = w.updateMessage(currentMsg, messageID, timestamp,
 		round, pinned, hidden, status)
 	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to updateMessage: %+v", err))
+		return errors.WithMessage(err, parentErr)
 	}
+	return nil
 }
 
 // UpdateFromMessageID is called whenever a message with the message ID is
@@ -293,33 +424,35 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
 // timestamp, round, pinned, and hidden are all nillable and may be updated
 // based upon the UUID at a later date. If a nil value is passed, then make
 // no update.
+//
+// Returns an error if the message cannot be updated. It must return
+// channels.NoMessageErr if the message does not exist.
 func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
 	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
-	status *channels.SentStatus) uint64 {
-	parentErr := errors.New("failed to UpdateFromMessageID")
+	status *channels.SentStatus) (uint64, error) {
+	parentErr := "failed to UpdateFromMessageID"
 
 	msgObj, err := impl.GetIndex(w.db, messageStoreName,
 		messageStoreMessageIndex, impl.EncodeBytes(messageID.Marshal()))
 	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Failed to get message by index: %+v", err))
-		return 0
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
+			return 0, errors.WithMessage(channels.NoMessageErr, parentErr)
+		}
+		return 0, errors.WithMessage(err, parentErr)
 	}
 
 	currentMsg, err := valueToMessage(msgObj)
 	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Failed to marshal Message: %+v", err))
-		return 0
+		return 0, errors.WithMessagef(err,
+			"%s Failed to marshal Message", parentErr)
 	}
 
 	uuid, err := w.updateMessage(currentMsg, &messageID, timestamp,
 		round, pinned, hidden, status)
 	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to updateMessage: %+v", err))
+		return 0, errors.WithMessage(err, parentErr)
 	}
-	return uuid
+	return uuid, nil
 }
 
 // buildMessage is a private helper that converts typical [channels.EventModel]
@@ -499,9 +632,11 @@ func (w *wasmModel) MuteUser(
 // valueToMessage is a helper for converting js.Value to Message.
 func valueToMessage(msgObj js.Value) (*Message, error) {
 	resultMsg := &Message{}
-	err := json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg)
-	if err != nil {
-		return nil, err
-	}
-	return resultMsg, nil
+	return resultMsg, json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg)
+}
+
+// valueToFile is a helper for converting js.Value to File.
+func valueToFile(fileObj js.Value) (*File, error) {
+	resultFile := &File{}
+	return resultFile, json.Unmarshal([]byte(utils.JsToJson(fileObj)), resultFile)
 }
diff --git a/indexedDb/impl/channels/implementation_test.go b/indexedDb/impl/channels/implementation_test.go
index 9577217901a13075bcd6b5bb3848d69d8577e81c..4b22b8300f5549a5dc2f3902f7ed95277777555f 100644
--- a/indexedDb/impl/channels/implementation_test.go
+++ b/indexedDb/impl/channels/implementation_test.go
@@ -10,9 +10,13 @@
 package main
 
 import (
+	"bytes"
 	"crypto/ed25519"
 	"encoding/json"
+	"errors"
 	"fmt"
+	cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
+	"gitlab.com/elixxir/crypto/fileTransfer"
 	"os"
 	"strconv"
 	"testing"
@@ -42,6 +46,79 @@ func dummyReceivedMessageCB(uint64, *id.ID, bool)      {}
 func dummyDeletedMessageCB(message.ID)                 {}
 func dummyMutedUserCB(*id.ID, ed25519.PublicKey, bool) {}
 
+// Happy path test for receiving, updating, getting, and deleting a File.
+func TestWasmModel_ReceiveFile(t *testing.T) {
+	testString := "TestWasmModel_ReceiveFile"
+	m, err := newWASMModel(testString, nil,
+		dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	testTs := time.Now()
+	testBytes := []byte(testString)
+	testStatus := cft.Downloading
+
+	// Insert a test row
+	fId := fileTransfer.NewID(testBytes)
+	err = m.ReceiveFile(fId, testBytes, testBytes, testTs, testStatus)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Attempt to get stored row
+	storedFile, err := m.GetFile(fId)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Spot check stored attribute
+	if !bytes.Equal(storedFile.Link, testBytes) {
+		t.Fatalf("Got unequal FileLink values")
+	}
+
+	// Attempt to updated stored row
+	newTs := time.Now()
+	newBytes := []byte("test")
+	newStatus := cft.Complete
+	err = m.UpdateFile(fId, nil, newBytes, &newTs, &newStatus)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Check that the update took
+	updatedFile, err := m.GetFile(fId)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Link should not have changed
+	if !bytes.Equal(updatedFile.Link, testBytes) {
+		t.Fatalf("Link should not have changed")
+	}
+	// Other attributes should have changed
+	if !bytes.Equal(updatedFile.Data, newBytes) {
+		t.Fatalf("Data should have updated")
+	}
+	if !updatedFile.Timestamp.Equal(newTs) {
+		t.Fatalf("TS should have updated, expected %s got %s",
+			newTs, updatedFile.Timestamp)
+	}
+	if updatedFile.Status != newStatus {
+		t.Fatalf("Status should have updated")
+	}
+
+	// Delete the row
+	err = m.DeleteFile(fId)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Check that the delete operation took and get provides the expected error
+	_, err = m.GetFile(fId)
+	if err == nil || !errors.Is(channels.NoMessageErr, err) {
+		t.Fatal(err)
+	}
+}
+
 // Happy path, insert message and look it up
 func TestWasmModel_GetMessage(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
@@ -54,7 +131,7 @@ func TestWasmModel_GetMessage(t *testing.T) {
 		if c != nil {
 			cs = "_withCipher"
 		}
-		testString := "TestWasmModel_msgIDLookup" + cs
+		testString := "TestWasmModel_GetMessage" + cs
 		t.Run(testString, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
@@ -235,7 +312,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 			}
 			eventModel.JoinChannel(testChannel)
 			eventModel.JoinChannel(testChannel2)
-			results, err2 := impl.Dump(eventModel.db, channelsStoreName)
+			results, err2 := impl.Dump(eventModel.db, channelStoreName)
 			if err2 != nil {
 				t.Fatal(err2)
 			}
@@ -243,7 +320,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 				t.Fatalf("Expected 2 channels to exist")
 			}
 			eventModel.LeaveChannel(testChannel.ReceptionID)
-			results, err = impl.Dump(eventModel.db, channelsStoreName)
+			results, err = impl.Dump(eventModel.db, channelStoreName)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/indexedDb/impl/channels/init.go b/indexedDb/impl/channels/init.go
index 9a8b940898ac30e81fa755e9d4053a21220ef66b..363440d541029007a6c167d94f8a1fcd66edf4fd 100644
--- a/indexedDb/impl/channels/init.go
+++ b/indexedDb/impl/channels/init.go
@@ -23,7 +23,7 @@ import (
 
 // currentVersion is the current version of the IndexedDb runtime. Used for
 // migration purposes.
-const currentVersion uint = 1
+const currentVersion uint = 2
 
 // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
 // The name should be a base64 encoding of the users public key. Returns the
@@ -62,6 +62,14 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 				oldVersion = 1
 			}
 
+			if oldVersion == 1 && newVersion >= 2 {
+				err := v2Upgrade(db)
+				if err != nil {
+					return err
+				}
+				oldVersion = 2
+			}
+
 			// if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 }
 			return nil
 		})
@@ -136,10 +144,22 @@ func v1Upgrade(db *idb.Database) error {
 	}
 
 	// Build Channel ObjectStore
-	_, err = db.CreateObjectStore(channelsStoreName, storeOpts)
+	_, err = db.CreateObjectStore(channelStoreName, storeOpts)
 	if err != nil {
 		return err
 	}
 
 	return nil
 }
+
+// v1Upgrade performs the v1 -> v2 database upgrade.
+//
+// This can never be changed without permanently breaking backwards
+// compatibility.
+func v2Upgrade(db *idb.Database) error {
+	_, err := db.CreateObjectStore(fileStoreName, idb.ObjectStoreOptions{
+		KeyPath:       js.ValueOf(pkeyName),
+		AutoIncrement: false,
+	})
+	return err
+}
diff --git a/indexedDb/impl/channels/model.go b/indexedDb/impl/channels/model.go
index d1dcee88b3efd5f4a877685366154caefaa46a82..e5d3e00aa5209985de60a77f5330a53208b1af88 100644
--- a/indexedDb/impl/channels/model.go
+++ b/indexedDb/impl/channels/model.go
@@ -18,8 +18,9 @@ const (
 	pkeyName = "id"
 
 	// Text representation of the names of the various [idb.ObjectStore].
-	messageStoreName  = "messages"
-	channelsStoreName = "channels"
+	messageStoreName = "messages"
+	channelStoreName = "channels"
+	fileStoreName    = "files"
 
 	// Message index names.
 	messageStoreMessageIndex   = "message_id_index"
@@ -73,3 +74,21 @@ type Channel struct {
 	Name        string `json:"name"`
 	Description string `json:"description"`
 }
+
+// File defines the IndexedDb representation of a single File.
+type File struct {
+	// Id is a unique identifier for a given File.
+	Id []byte `json:"id"` // Matches pkeyName
+
+	// Data stores the actual contents of the File.
+	Data []byte `json:"data"`
+
+	// Link contains all the information needed to download the file data.
+	Link []byte `json:"link"`
+
+	// Timestamp is the last time the file data, link, or status was modified.
+	Timestamp time.Time `json:"timestamp"`
+
+	// Status of the file in the event model.
+	Status uint8 `json:"status"`
+}
diff --git a/indexedDb/impl/utils_test.go b/indexedDb/impl/utils_test.go
index 6b6da6ff36ee8be5412146443093ee7d85c07c4f..00e235834c44788905af73cafd7448961708a4dc 100644
--- a/indexedDb/impl/utils_test.go
+++ b/indexedDb/impl/utils_test.go
@@ -40,6 +40,20 @@ func TestGetIndex_NoMessageError(t *testing.T) {
 	}
 }
 
+// Test simple put on empty DB is successful
+func TestPut(t *testing.T) {
+	objectStoreName := "messages"
+	db := newTestDB(objectStoreName, "index", t)
+	testValue := js.ValueOf(make(map[string]interface{}))
+	result, err := Put(db, objectStoreName, testValue)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	if !result.Equal(js.ValueOf(1)) {
+		t.Fatalf("Failed to generate autoincremented key")
+	}
+}
+
 // newTestDB creates a new idb.Database for testing.
 func newTestDB(name, index string, t *testing.T) *idb.Database {
 	// Attempt to open database object
diff --git a/indexedDb/worker/channels/implementation.go b/indexedDb/worker/channels/implementation.go
index 4d51b97e6a2941e07849c0e2e405539ccc86a8f7..9639bc185095e74cbf4b3e63256fead39bbcef82 100644
--- a/indexedDb/worker/channels/implementation.go
+++ b/indexedDb/worker/channels/implementation.go
@@ -263,9 +263,12 @@ type MessageUpdateInfo struct {
 // messageID, timestamp, round, pinned, and hidden are all nillable and may be
 // updated based upon the UUID at a later date. If a nil value is passed, then
 // make no update.
+//
+// Returns an error if the message cannot be updated. It must return
+// [channels.NoMessageErr] if the message does not exist.
 func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
 	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
-	status *channels.SentStatus) {
+	status *channels.SentStatus) error {
 	msg := MessageUpdateInfo{UUID: uuid}
 	if messageID != nil {
 		msg.MessageID = *messageID
@@ -294,12 +297,33 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
 
 	data, err := json.Marshal(msg)
 	if err != nil {
-		jww.ERROR.Printf(
-			"[CH] Could not JSON marshal payload for UpdateFromUUID: %+v", err)
-		return
+		return errors.Errorf(
+			"could not JSON marshal payload for UpdateFromUUID: %+v", err)
 	}
 
-	w.wm.SendMessage(UpdateFromUUIDTag, data, nil)
+	errChan := make(chan error)
+	w.wm.SendMessage(UpdateFromUUIDTag, data, func(data []byte) {
+		if data != nil {
+			errChan <- errors.New(string(data))
+		} else {
+			errChan <- nil
+		}
+	})
+
+	select {
+	case err = <-errChan:
+		return err
+	case <-time.After(worker.ResponseTimeout):
+		return errors.Errorf("timed out after %s waiting for response from "+
+			"the worker about UpdateFromUUID", worker.ResponseTimeout)
+	}
+}
+
+// UuidError is JSON marshalled and sent to the worker for
+// [wasmModel.UpdateFromMessageID].
+type UuidError struct {
+	UUID  uint64 `json:"uuid"`
+	Error []byte `json:"error"`
 }
 
 // UpdateFromMessageID is called whenever a message with the message ID is
@@ -313,7 +337,7 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
 // no update.
 func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
 	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
-	status *channels.SentStatus) uint64 {
+	status *channels.SentStatus) (uint64, error) {
 
 	msg := MessageUpdateInfo{MessageID: messageID, MessageIDSet: true}
 	if timestamp != nil {
@@ -339,33 +363,34 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
 
 	data, err := json.Marshal(msg)
 	if err != nil {
-		jww.ERROR.Printf("[CH] Could not JSON marshal payload for "+
+		return 0, errors.Errorf("could not JSON marshal payload for "+
 			"UpdateFromMessageID: %+v", err)
-		return 0
 	}
 
 	uuidChan := make(chan uint64)
+	errChan := make(chan error)
 	w.wm.SendMessage(UpdateFromMessageIDTag, data,
 		func(data []byte) {
-			var uuid uint64
-			err = json.Unmarshal(data, &uuid)
-			if err != nil {
-				jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
-					"UpdateFromMessageID: %+v", err)
-				uuidChan <- 0
+			var ue UuidError
+			if err = json.Unmarshal(data, &ue); err != nil {
+				errChan <- errors.Errorf("could not JSON unmarshal response "+
+					"to UpdateFromMessageID: %+v", err)
+			} else if ue.Error != nil {
+				errChan <- errors.New(string(ue.Error))
+			} else {
+				uuidChan <- ue.UUID
 			}
-			uuidChan <- uuid
 		})
 
 	select {
 	case uuid := <-uuidChan:
-		return uuid
+		return uuid, nil
+	case err = <-errChan:
+		return 0, err
 	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
+		return 0, errors.Errorf("timed out after %s waiting for response from "+
 			"the worker about UpdateFromMessageID", worker.ResponseTimeout)
 	}
-
-	return 0
 }
 
 // GetMessageMessage is JSON marshalled and sent to the worker for
diff --git a/main.go b/main.go
index 05277d5985e1884275fb1b53476c0c233a6dd358..c109ff54f41fc0c3acb34bd7d3c848013237360c 100644
--- a/main.go
+++ b/main.go
@@ -89,9 +89,15 @@ func main() {
 	js.Global().Set("GetShareUrlType", js.FuncOf(wasm.GetShareUrlType))
 	js.Global().Set("ValidForever", js.FuncOf(wasm.ValidForever))
 	js.Global().Set("IsNicknameValid", js.FuncOf(wasm.IsNicknameValid))
+	js.Global().Set("GetNoMessageErr", js.FuncOf(wasm.GetNoMessageErr))
+	js.Global().Set("CheckNoMessageErr", js.FuncOf(wasm.CheckNoMessageErr))
 	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/channels.go b/wasm/channels.go
index a775928eb820439bdb1a4739d8011b1b952cf7de..307fe5cee304ff580ba1726b8036ce81268cc991 100644
--- a/wasm/channels.go
+++ b/wasm/channels.go
@@ -256,7 +256,11 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any {
 //     using [Cmix.GetID].
 //   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[2] - A function that initialises and returns a Javascript object
+//   - args[2] - JSON of an array of integers of [channels.ExtensionBuilder]
+//     IDs. The ID can be retrieved from an object with an extension builder
+//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
+//     using extension builders. Example: `[2,11,5]` (Uint8Array).
+//   - args[3] - A function that initialises and returns a Javascript object
 //     that matches the [bindings.EventModel] interface. The function must match
 //     the Build function in [bindings.EventModelBuilder].
 //
@@ -264,10 +268,13 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any {
 //   - Javascript representation of the [ChannelsManager] object.
 //   - Throws a TypeError if creating the manager fails.
 func NewChannelsManager(_ js.Value, args []js.Value) any {
+	cmixId := args[0].Int()
 	privateIdentity := utils.CopyBytesToGo(args[1])
-	em := newEventModelBuilder(args[2])
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[2])
+	em := newEventModelBuilder(args[3])
 
-	cm, err := bindings.NewChannelsManager(args[0].Int(), privateIdentity, em)
+	cm, err := bindings.NewChannelsManager(
+		cmixId, privateIdentity, extensionBuilderIDsJSON, em)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
@@ -324,7 +331,11 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 //   - args[1] - Path to Javascript file that starts the worker (string).
 //   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[3] - The received message callback, which is called everytime a
+//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
+//     IDs. The ID can be retrieved from an object with an extension builder
+//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
+//     using extension builders. Example: `[2,11,5]` (Uint8Array).
+//   - args[4] - The received message callback, which is called everytime a
 //     message is added or changed in the database. It is a function that takes
 //     in the same parameters as [channels.MessageReceivedCallback]. On the
 //     Javascript side, the UUID is returned as an int and the channelID as a
@@ -332,15 +343,15 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 //     the UUID. The channel ID is provided so that the recipient can filter if
 //     they want to the processes the update now or not. An "update" bool is
 //     present which tells you if the row is new or if it is an edited old row.
-//   - args[4] - The deleted message callback, which is called everytime a
+//   - args[5] - The deleted message callback, which is called everytime a
 //     message is deleted from the database. It is a function that takes in the
 //     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
 //     side, the message ID is returned as a Uint8Array.
-//   - args[5] - The muted user callback, which is called everytime a user is
+//   - args[6] - The muted user callback, which is called everytime a user is
 //     muted or unmuted. It is a function that takes in the same parameters as
 //     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
 //     user public key are returned as Uint8Array.
-//   - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this
+//   - args[7] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
@@ -352,10 +363,11 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	wasmJsPath := args[1].String()
 	privateIdentity := utils.CopyBytesToGo(args[2])
-	messageReceivedCB := args[3]
-	deletedMessageCB := args[4]
-	mutedUserCB := args[5]
-	cipherID := args[6].Int()
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	messageReceivedCB := args[4]
+	deletedMessageCB := args[5]
+	mutedUserCB := args[6]
+	cipherID := args[7].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
@@ -363,7 +375,8 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	}
 
 	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
-		messageReceivedCB, deletedMessageCB, mutedUserCB, cipher)
+		extensionBuilderIDsJSON, messageReceivedCB, deletedMessageCB,
+		mutedUserCB, cipher)
 }
 
 // NewChannelsManagerWithIndexedDbUnsafe creates a new [ChannelsManager] from a
@@ -384,7 +397,11 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //   - args[1] - Path to Javascript file that starts the worker (string).
 //   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[3] - The received message callback, which is called everytime a
+//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
+//     IDs. The ID can be retrieved from an object with an extension builder
+//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
+//     using extension builders. Example: `[2,11,5]` (Uint8Array).
+//   - args[4] - The received message callback, which is called everytime a
 //     message is added or changed in the database. It is a function that takes
 //     in the same parameters as [indexedDb.MessageReceivedCallback]. On the
 //     Javascript side, the UUID is returned as an int and the channelID as a
@@ -392,11 +409,11 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //     the UUID. The channel ID is provided so that the recipient can filter if
 //     they want to the processes the update now or not. An "update" bool is
 //     present which tells you if the row is new or if it is an edited old row.
-//   - args[4] - The deleted message callback, which is called everytime a
+//   - args[5] - The deleted message callback, which is called everytime a
 //     message is deleted from the database. It is a function that takes in the
 //     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
 //     side, the message ID is returned as a Uint8Array.
-//   - args[5] - The muted user callback, which is called everytime a user is
+//   - args[6] - The muted user callback, which is called everytime a user is
 //     muted or unmuted. It is a function that takes in the same parameters as
 //     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
 //     user public key are returned as Uint8Array.
@@ -410,17 +427,19 @@ func NewChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	wasmJsPath := args[1].String()
 	privateIdentity := utils.CopyBytesToGo(args[2])
-	messageReceivedCB := args[3]
-	deletedMessageCB := args[4]
-	mutedUserCB := args[5]
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	messageReceivedCB := args[4]
+	deletedMessageCB := args[5]
+	mutedUserCB := args[6]
 
 	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
-		messageReceivedCB, deletedMessageCB, mutedUserCB, nil)
+		extensionBuilderIDsJSON, messageReceivedCB, deletedMessageCB,
+		mutedUserCB, nil)
 }
 
 func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
-	privateIdentity []byte, messageReceivedCB, deletedMessageCB, mutedUserCB js.Value,
-	cipher *bindings.ChannelDbCipher) any {
+	privateIdentity, extensionBuilderIDsJSON []byte, messageReceivedCB,
+	deletedMessageCB, mutedUserCB js.Value, cipher *bindings.ChannelDbCipher) any {
 
 	messageReceived := func(uuid uint64, channelID *id.ID, update bool) {
 		messageReceivedCB.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update)
@@ -440,7 +459,7 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.NewChannelsManagerGoEventModel(
-			cmixID, privateIdentity, model)
+			cmixID, privateIdentity, extensionBuilderIDsJSON, model)
 		if err != nil {
 			reject(utils.JsTrace(err))
 		} else {
@@ -577,7 +596,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string,
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.LoadChannelsManagerGoEventModel(
-			cmixID, storageTag, model)
+			cmixID, storageTag, model, nil)
 		if err != nil {
 			reject(utils.JsTrace(err))
 		} else {
@@ -1747,6 +1766,30 @@ func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) a
 // Event Model Logic                                                          //
 ////////////////////////////////////////////////////////////////////////////////
 
+// GetNoMessageErr returns the error channels.NoMessageErr, which must be
+// returned by EventModel methods (such as EventModel.UpdateFromUUID,
+// EventModel.UpdateFromMessageID, and EventModel.GetMessage) when the message
+// cannot be found.
+//
+// Returns:
+//   - channels.NoMessageErr error message (string).
+func GetNoMessageErr(js.Value, []js.Value) any {
+	return bindings.GetNoMessageErr()
+}
+
+// CheckNoMessageErr determines if the error returned by an EventModel function
+// indicates that the message or item does not exist. It returns true if the
+// error contains channels.NoMessageErr.
+//
+// Parameters:
+//   - args[0] - Error to check (Error).
+//
+// Returns
+//   - True if the error contains channels.NoMessageErr (boolean).
+func CheckNoMessageErr(_ js.Value, args []js.Value) any {
+	return bindings.CheckNoMessageErr(utils.JsErrorToJson(args[0]))
+}
+
 // eventModelBuilder adheres to the [bindings.EventModelBuilder] interface.
 type eventModelBuilder struct {
 	build func(args ...any) js.Value
@@ -1944,13 +1987,22 @@ func (em *eventModel) ReceiveReaction(channelID, messageID, reactionTo []byte,
 //   - uuid - The unique identifier of the message in the database (int).
 //   - messageUpdateInfoJSON - JSON of [bindings.MessageUpdateInfo]
 //     (Uint8Array).
-func (em *eventModel) UpdateFromUUID(uuid int64, messageUpdateInfoJSON []byte) {
-	em.updateFromUUID(uuid, utils.CopyBytesToJS(messageUpdateInfoJSON))
+//
+// Returns:
+//   - Returns an error if the message cannot be updated. It must return the
+//     error from [GetNoMessageErr] if the message does not exist.
+func (em *eventModel) UpdateFromUUID(
+	uuid int64, messageUpdateInfoJSON []byte) error {
+	err := em.updateFromUUID(uuid, utils.CopyBytesToJS(messageUpdateInfoJSON))
+	return js.Error{Value: err}
 }
 
 // UpdateFromMessageID is called whenever a message with the message ID is
 // modified.
 //
+// Note for developers: The internal Javascript function must return JSON of
+// [UuidAndError], which includes the returned UUID or an error.
+//
 // Parameters:
 //   - messageID - The bytes of the [channel.MessageID] of the received message
 //     (Uint8Array).
@@ -1960,16 +2012,31 @@ func (em *eventModel) UpdateFromUUID(uuid int64, messageUpdateInfoJSON []byte) {
 // Returns:
 //   - A non-negative unique uuid for the modified message by which it can be
 //     referenced later with [EventModel.UpdateFromUUID] int).
+//   - Returns an error if the message cannot be updated. It must return the
+//     error from [GetNoMessageErr] if the message does not exist.
 func (em *eventModel) UpdateFromMessageID(
-	messageID []byte, messageUpdateInfoJSON []byte) int64 {
-	return int64(em.updateFromMessageID(utils.CopyBytesToJS(messageID),
-		utils.CopyBytesToJS(messageUpdateInfoJSON)).Int())
+	messageID []byte, messageUpdateInfoJSON []byte) (int64, error) {
+	uuidAndErrorBytes := utils.CopyBytesToGo(em.updateFromMessageID(
+		utils.CopyBytesToJS(messageID),
+		utils.CopyBytesToJS(messageUpdateInfoJSON)))
+
+	var uae UuidAndError
+	err := json.Unmarshal(uuidAndErrorBytes, &uae)
+	if err != nil {
+		return 0, err
+	}
+
+	if uae.Error != "" {
+		return 0, errors.New(uae.Error)
+	}
+
+	return uae.UUID, nil
 }
 
 // GetMessage returns the message with the given [channel.MessageID].
 //
 // Note for developers: The internal Javascript function must return JSON of
-// MessageAndError, which includes the returned [channels.ModelMessage] or any
+// [MessageAndError], which includes the returned [channels.ModelMessage] or any
 // error that occurs during lookup.
 //
 // Parameters:
@@ -2020,6 +2087,21 @@ func (em *eventModel) MuteUser(channelID, pubkey []byte, unmute bool) {
 		utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(pubkey), unmute)
 }
 
+// UuidAndError contains a UUID returned by an eventModel method or any possible
+// error that occurs. Only one field should be present at a time.
+//
+// Example JSON:
+//
+//	{ "uuid": 5, }
+//
+// Or:
+//
+//	{ "error": "An error occurred." }
+type UuidAndError struct {
+	UUID  int64  `json:"uuid,omitempty"`
+	Error string `json:"error,omitempty"`
+}
+
 // MessageAndError contains a message returned by eventModel.GetMessage or any
 // possible error that occurs during lookup. Only one field should be present at
 // a time; if an error occurs, ModelMessage should be empty.
diff --git a/wasm/channelsFileTransfer.go b/wasm/channelsFileTransfer.go
new file mode 100644
index 0000000000000000000000000000000000000000..5155e4ad4668bef0f3ad52f52efec748e7dbb0ff
--- /dev/null
+++ b/wasm/channelsFileTransfer.go
@@ -0,0 +1,569 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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{
+		"GetExtensionBuilderID": js.FuncOf(cft.GetExtensionBuilderID),
+		"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)
+}
+
+// GetExtensionBuilderID returns the ID of the extension builder in the tracker.
+// Pass this ID into the channel manager creator to use file transfer manager in
+// conjunction with channels.
+//
+// Returns:
+//   - Extension builder ID (int).
+func (cft *ChannelsFileTransfer) GetExtensionBuilderID(js.Value, []js.Value) any {
+	return cft.api.GetExtensionBuilderID()
+}
+
+// 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 file download.
+//
+// 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 [channelsFileTransfer.Complete].
+//
+// 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 0000000000000000000000000000000000000000..d697de63b3bb23049b7e933355a09eeb749955c1
--- /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 b2d6f4894014b15873b2649118e2af09a8cd7091..5b8e7a6dced810bb861c14c45bc41e55fc2385ae 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{}
 )